/**
 * @module NGS
 * @author Kevin MOU
 */
(function($) {
	var dbtimeObject, localTime, localTimezone, localTimeSyncTimer, localTimeSyncInterval = 1000;

	/**
	 * __系统顶级类：$F__，提供一些常用函数与方法
	 * @class $F
	 */
	var $F = {
		lockScreenTimer: function () {
			const project = $E.getActiveProject()
			let timeout = project.getParam('ATMSLK');
			if(!timeout){
				return;
			}
			this.dataService('SystemDo2', {
				data : $F.sqlbean('staff.autolock.whitelist', project.token,{
					C1: $E.staff.id
				}),
				digest : project.digest,
				async : false,
				success : function(data) {
					if(data.value[0].count <= 0){
						document.addEventListener("mousemove", resetTimer);
						document.addEventListener("keypress", resetTimer);
						let time;
						function startTimer() {
							time = setTimeout(function () {
								document.removeEventListener("mousemove", resetTimer);
								document.removeEventListener("keypress", resetTimer);
								// 这里可以加入屏幕锁定的操作
								$E.getActiveProject().lockScreen();
							}, timeout * 1000);
						}

						function resetTimer() {
							clearTimeout(time);
							startTimer();
						}
						startTimer();
					}
				}
			})

		},
		/**
		 * 获得随机字符串
		 * @method randomString
		 * @param {integer} length 字符串长度
		 * @return {string} 随机字符串
		 */
		randomString : function(len) {
			len = len || 32;
			var $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789';
			var s = '';
			for (var i = 0; i < len; i++) {
				s += $chars.charAt(Math.floor(Math.random() * $chars.length));
			}
			return s;
		},

		/**
		 * 随机获得一个[A-Z][0-9]的字符
		 * @method randomChar
		 * @param length {integer} 随机字符的个数，默认为1
		 * @return {string} 随机字符串
		 */
		randomChar : function(length) {
			var $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
			var id = '';
			for (var i = 0; i < (length || 1); i++) {
				id += $chars.charAt(Math.floor(Math.random() * $chars.length));
			}
			return id;
		},

		/**
		 * 获得一个不存在数据表中的6位ID
		 * @method ID6Gen
		 * @param token
		 * @param digest
		 * @param key {string} SQLKEY
		 * @param [mainId] {string} 主ID，可选入参
		 * @return {string} 6位随机字符串
		 */
		ID6Gen : function(token, digest, key, mainId) {
			var a = this,
				id = $F.randomChar(6),
				requestData;
			if (key) {
				if (mainId)
					requestData = $F.sqlbean(key, token, [ mainId, id ]);
				else
					requestData = $F.sqlbean(key, token, [ id ]);
				a.dataService('SystemDo2', {
					data : requestData,
					async : false,
					digest : digest,
					success : function(data) {
						if (data.code < 1)
							id = a.ID6Gen(token, digest, key, mainId);
					}
				})
			}
			return id;
		},

		/**
		 * 数字格式化字符串方法
		 *
		 *	Format examples:
		 *		formatNumber(0,'');
		 * 		formatNumber(12432.21, '#,###');		// 货币分隔法
		 * 		formatNumber(12432.21, '#,###.000#');	// 货币分割法，且小数点保留三位，不足三位小数补0, 尾数使用四舍五入
		 * 		formatNumber(12432, '#,###.00');		// 货币分割法，且小数点保留两位，不足两位小数补0
		 * 		formatNumber('12432.415', '#,###.0#');	// 货币分割法，且小数点保留一位, 尾数使用四舍五入
		 * 		formatNumber('12345', '000000');		// 强制为6为数值，不足补0
		 * 		formatNumber('22.90', '[$]##.##');		// 小数点保留不多于两位，且是美元$作为符号
		 * 		formatNumber('0.124'. '#.00#%');		// 百分比转化，且小数点保留两位,不足两位小数补0, 尾数使用四舍五入
		 *
		 * @method formatNumber
		 * @param {number || string} number 格式化对象，可以是数字或者字符串
		 * @param {string} pattern 格式
		 * @return {string} 格式化后的数字字符串
		 */
		formatNumber : function(number, pattern) {
			return String.formatNumber(number, pattern);
		},

		/**
		 * 格式化字符串，通常用于进行字符串隐秘处理
		 * @method formatString
		 * @param {string} source 源字符串
		 * @param {string} pattern 格式
		 * @return {string} 格式化后的字符串
		 *
		 */
		formatString : function(vSourceString, vFormatString) {
			// 格式化字符
			var iIndex = 0,
				mReturnStr = "";
			var iSouceLen = vSourceString.length,
				iFormatLen = vFormatString.length;
			for (var i = 0; i < iFormatLen; i++) {
				var mStr = vFormatString.substr(i, 1);
				if (mStr == "#") {
					if (iIndex < iSouceLen) {
						mReturnStr = mReturnStr + vSourceString.substr(iIndex, 1);
						iIndex++;
					}
				} else if (mStr == "*") {
					if (iIndex < iSouceLen) {
						mReturnStr = mReturnStr + "*";
						iIndex++;
					}
				} else {
					mReturnStr = mReturnStr + mStr;
				}
			}
			if (iIndex < iSouceLen) {
				mReturnStr += vSourceString.substr(iIndex);
			}
			return mReturnStr;
		},

		/**
		 * 将对象输出成字符串
		 * @method display
		 * @param object {any} 对象
		 * @return {string}
		 */
		display : function(object) {
			if (typeof (object) == 'undefined' || object == null)
				return '';
			else if (typeof (object) == 'string') {
				if (object.length > 36)
					object = object.substring(0, 36) + '...';
				return '\'' + object + '\'';
			} else if (typeof (object) == 'boolean' || typeof (object) == 'number')
				return object;
			else if (typeof (object.splice) == 'function') {
				// this is array
				return '[' + object.join(',') + ']';
			} else {
				// this is object
				var s = '{',
					i = 0;
				for (var o in object) {
					if (i > 0)
						s += ', ';
					s += '\'' + o + '\'';
					s += ':';
					s += this.display(object[o]);
					i++;
				}
				s += '}';
				return s;
			}
		},

		logGroup : function(c, e) {
			if ($F.localProxyReady()) {
				if (e && e.length > 30000) { // vblocalproxy限制发送过去消息长度不能大于65534, 考虑到中文字符，所以只截取30000字符长度
					e = e.substr(0, 30000) + '...';
				}
				var data = {
					"module" : "log",
					"category" : "NGSClient",
					"loglevel" : 4,
					"content" : ('[' + c + "] " + e)
				};
				$E.ws.local.proxy.send(JSON.stringify(data));
			}
			if ($E.debug)
				console.group('[' + c + "] " + e);
			this.clientLogger.logToServer('[' + c + "] " + e, 'group');
			return this;
		},

		logGroupEnd : function(c, e, obj) {
			if ($E.debug) {
				if (c != undefined)
					this.log(c, e, obj);
				console.groupEnd();
			}
			this.clientLogger.logToServer('', 'groupEnd');
		},

		/**
		 * 消息日志函数
		 * @method log
		 * @param {string} c 日志头
		 * @param {string} e 日志消息内容
		 * @param {object} [obj] 日志内容对象
		 * @param {boolean} [local] 是否只在本地记录
		 */
		log : function(c, e, obj, local) {
			// LogOCX level:
			// 1: system
			// 2: error
			// 3: warning
			// 4: info
			if ($F.localProxyReady()) {
				if (e && e.length > 30000) { // vblocalproxy限制发送过去消息长度不能大于65534, 考虑到中文字符，所以只截取30000字符长度
					e = e.substr(0, 30000) + '...';
				}
				var data = {
					"module" : "log",
					"category" : "NGSClient",
					"loglevel" : 4,
					"content" : ('[' + c + "] " + e)
				};
				//    				if (obj)
				//    					data.object = obj;
				$E.ws.local.proxy.send(JSON.stringify(data));
			}
			var logToServer = true;
			if (obj !== undefined) {
				if (typeof(obj) === 'boolean') {
					logToServer = !obj;
					if ($E.debug)
						console.log('[' + c + "] " + e);
				} else {
					if (local) {
						logToServer = false;
					}
					if ($E.debug)
						console.log('[' + c + "] " + e, obj);
				}
			} else {
				if ($E.debug)
					console.log('[' + c + "] " + e);
			}
			if (logToServer) {
				var toServerContent = '[' + c + "] " + e;
				if (obj && typeof(obj) === 'string') {
					toServerContent += obj;
				}
				$F.clientLogger.logToServer(toServerContent);
			}
			return this;
		},

		/**
		 * 本地日志批量发送到服务器上
		 */
		clientLogger : {
			enabled : false,
			logServerUrl : 'clientLogger',
			logCacheSize : 100,
			logCache : [],
			spacer : '',

			softphoneLogEnabled: true,
			softphoneLogCacheSize : 10,
			softphoneLogCache : [],
			softphoneLogSpacer: '',

			logToServer : function(content, type) {
				if (this.enabled) { // root-context中配置的是否开启客户端日志
					if (!$.isEmptyObject($E.projects) && $E.getActiveProject().clientLoggerEnabled) { // admin端是否开启当前坐席的客户端日志
						type = type || 'debug';
						if (type == 'groupEnd') {
							if (this.spacer.length >= 2) {
								this.spacer = this.spacer.substring(0, this.spacer.length - 2);
							}
						}
						this.logCache.push(this.spacer + type + ' - ' + new Date().format('hh:mm:ss.S') + ' ' + content);
						if (type == 'group') {
							this.spacer += '  ';
						}
						if (this.logCache.length >= this.logCacheSize) {
							$.ajax({
								type : 'POST',
								url : this.logServerUrl,
								dataType : 'json',
								data : JSON.stringify({
									staffId : $E.staff.id,
									token : $E.getActiveProject().token,
									logs : this.logCache
								}),
								contentType : 'application/json;charset=UTF-8'
							});
							this.logCache = [];
						}
					}
				}
			},

			softphoneLogToServer : function(content, type) {
				if (this.softphoneLogEnabled) { // root-context中配置的是否开启客户端日志
					//if (!$.isEmptyObject($E.projects) && $E.getActiveProject().clientLoggerEnabled) { // admin端是否开启当前坐席的客户端日志
					type = type || 'debug';
					let flushlog = false;
					if (type === "flushlog") {
						flushlog = true;
					}
					if (!flushlog) {
						if (type == 'groupEnd') {
							if (this.softphoneLogSpacer.length >= 2) {
								this.softphoneLogSpacer = this.softphoneLogSpacer.substring(0, this.softphoneLogSpacer.length - 2);
							}
						}
						this.softphoneLogCache.push(this.softphoneLogSpacer + type + ' - ' + new Date().format('hh:mm:ss.S') + ' ' + content);
						if (type == 'group') {
							this.softphoneLogSpacer += '  ';
						}
					}
					if ((flushlog && this.softphoneLogCache.length > 0) || this.softphoneLogCache.length >= this.softphoneLogCacheSize) {
						try {
							$.ajax({
								type: 'POST',
								url: this.logServerUrl,
								dataType: 'json',
								data: JSON.stringify({
									staffId: $E.staff.id,
									token: $E.getActiveProject().token,
									logs: this.softphoneLogCache
								}),
								contentType: 'application/json;charset=UTF-8'
							});
							this.softphoneLogCache = [];
						} catch (e) {
							console.log("softphoneLogToServer", e.message, e.stack);
						}
					}
					//}
				}

				// 根据配置是否有额外日志输出的接口，决定是否要输出日志
				$F.extraLog({
					type: 'cti',
					context: content
				});
			}
		},

		/**
		 * 额外日志输出方法，根据系统参数EXTLOG 决定是否调用日志记录接口
		 * @param params
		 *  type 类型 默认ngs
		 *  context 日志内容
		 *  pageId 页面id
		 *  controlId 控件id
		 */
		extraLog(params) {
			const project = $E.getActiveProject();
			const externalLogUrl = project.getParam('EXTLOG');
			if (externalLogUrl && params.content) {
				$.ajax({
					type: 'POST',
					url: externalLogUrl,
					dataType: 'json',
					data: JSON.stringify({
						staffId: $E.staff.id,
						agentId: $E.softphone ? $E.softphone.agentId : $E.staff.agentId,
						extension: $E.softphone ? $E.softphone.extension : '',
						dateTime: new Date().format(),
						type: params.type || 'ngs',
						context: params.context,
						pageId: params.pageId || '',
						controlId: params.controlId || ''
					}),
					contentType: 'application/json;charset=UTF-8'
				});
			}
		},


		/**
		 * 错误消息日志函数
		 * @method err
		 * @param {string} source 生产日志的对象
		 * @param {string} event 日志消息内容
		 */
		err : function(...args) {
			let name = 'Elite.NGS',
				cause = '',
				e;
			if (args.length === 0)
				return;
			else if (args.length === 1)
				e = arguments[0]
			else if (args.length === 2) {
				name = arguments[0];
				e = arguments[1];
			} else {
				name = arguments[0];
				cause = arguments[1];
				e = arguments[2];
			}

			this.notify(cause || e, -1);
			if ($F.localProxyReady()) {
				var msg = cause + e;
				if (msg && msg.length > 30000) { // vblocalproxy限制发送过去消息长度不能大于65534, 考虑到中文字符，所以只截取30000字符长度
					msg = msg.substr(0, 30000) + '...';
				}
				var data = {
					"module" : "log",
					"category" : "NGSClient",
					"loglevel" : 2,
					"content" : ('[' + name + "] " + msg)
				};
				$E.ws.local.proxy.send(JSON.stringify(data));
			}

			if (window.$E && $E.device == 'mobile' && $E.debug) {
				alert('[' + name + "] " + (cause || e));
			}

			console.error('[' + name + "] " + cause, e);
			// this.clientLogger.logToServer('[' + name + "] " + cause, 'error');
			return this;
		},

		localProxyReady: function() {
			return !!(window.$E && $E.ws && $E.ws.local && $E.ws.local.proxy);
		},

		/**
		 * 消息对话框
		 * @method msg
		 * @param message {string} 消息内容
		 * @param [callback] {function} 关闭对话框后的回调方法
		 * @param [state] {integer} 消息类型：0=消息提示；1=正确提示；2=警告提示；3=错误提示
		 */
		msg : function(e, callback, state, htmlallow) {
			var dialog;
			if (window.$E && $E.device == 'mobile')
				dialog = this.createMobileMessageDialog((window.$E && $E.name) || 'EliteCRM', e, htmlallow);
			else
				dialog = this.createMessageDialog((window.$E && $E.name) || 'EliteCRM', e, state || 0, htmlallow);
			dialog.on('keydown', function(e) {
				if (e.keyCode == 27)
					Helper.masker.hide(dialog);
			}).on('click', function(e) {
				e.stopPropagation();
			})

			Helper.masker.show(dialog, {
				onOpen : function() {
					$('.js_ok', dialog).focus();
				},
				onClose : function() {
					dialog.off('click');
					if ($.isFunction(callback))
						callback.call(dialog);
				}
			});
		},

		/**
		 * 正确提示对话框
		 * @method info
		 * @param message {string} 提示消息内容
		 * @param [callback] {callback} 关闭对话框后的回调方法
		 * @param [htmlallow] {boolean}  是否允许使用html代码，默认关闭
		 */
		info : function(e, callback, htmlallow) {
			this.msg(e, callback, 1, htmlallow);
		},

		/**
		 * 警告提示对话框
		 * @method warn
		 * @param message {string} 提示消息内容
		 * @param [callback] {callback} 关闭对话框后的回调方法
		 * @param [htmlallow] {boolean}  是否允许使用html代码，默认关闭
		 */
		warn : function(e, callback, htmlallow) {
			this.msg(e, callback, 2, htmlallow);
		},

		/**
		 * 错误提示对话框
		 * @method alert
		 * @param message {string} 提示消息内容
		 * @param [callback] {callback} 关闭对话框后的回调方法
		 * @param [htmlallow] {boolean}  是否允许使用html代码，默认关闭
		 */
		alert : function(e, callback, htmlallow) {
			this.msg(e, callback, 3, htmlallow);
		},

		isMobileDevice : function() {
			return $('body').attr('data-device') === 'mobile';
		},

		/**
		 * 创建一个对话框
		 * @method createMessageDialog
		 * @param title {string} 标题
		 * @param message {string} 消息
		 * @param state {integer} 类型, 0=msg, 1=info, 2=warn, 3=alert, 4=confirm
		 * @return {dom} 对话框DOM
		 */
		createMessageDialog : function(title, message, state, htmlallow) {
			var a = this,
				dialog = $("<div class='sys_dialog' tabIndex=-1 />").appendTo('body');
			var close = function() {
				Helper.masker.hide(dialog)
				dialog.remove();
			}

			var title = $("<div class='title unselectable' />").text(title).appendTo(dialog);
			$("<a href='javascript:;' class='close_icon'></a>").appendTo(dialog).on('click', close);
			var content = $("<div class='content'></div>").appendTo(dialog);
			if (htmlallow) {
				content.html(message);
			} else {
				if (typeof(message) !== 'string') {
					message = new String(message);
				}
				var cs = message.replace(/<br(\s?)\/>/gi, '\n').replace(/<p(\s?)\/>/gi, '\n').split('\n');
				if (cs.length < 2)
					content.text(cs);
				else {
					$.each(cs, function(i, v) {
						content.append($('<span style="display:block"/>').text(v));
					})
				}
			}
			switch (state) {
				case 1:
					$('<span class="icon info"><i class="fas fa-check-circle"></i></span>').appendTo(content);
					break;
				case 2:
					$('<span class="icon warn"><i class="fas fa-exclamation-triangle"></i></span>').appendTo(content);
					break;
				case 3:
					$('<span class="icon error"><i class="fas fa-exclamation-circle"></i></span>').appendTo(content);
					break;
				case 4:
					$('.close_icon').hide();
					$('<span class="icon confirm"><i class="fas fa-question-circle"></i></span>').appendTo(content);
					break;
				default:
					$('<span class="icon msg"><i class="fas fa-info-circle"></i></span>').appendTo(content);
			}

			var btnbar = $("<div class='btnbar'></div>").appendTo(dialog);
			$('<button class="unselectable ' + (a.isMobileDevice() ? 'mobile' : 'chrome') + ' js_ok">' + a.lang('message.ok') + '</button>').appendTo(btnbar).on('click', close);

			if (dialog.draggable)
				return dialog.draggable({
					handle : title
				});
			else
				return dialog;
		},

		/**
		 * 创建一个基于移动手机的对话框
		 * @method createMobileMessageDialog
		 * @param title {string} 标题
		 * @param message {string} 消息
		 * @return {dom} 对话框DOM
		 */
		createMobileMessageDialog : function(title, message, htmlallow) {
			var a = this,
				dialog = $("<div class='mobile_dialog unselectable'/>").appendTo('body');
			var close = function() {
				Helper.masker.hide(dialog)
				dialog.remove();
			}
			var title = $("<div class='title' />").text(title).appendTo(dialog);
			var content = $("<div class='content'></div>").appendTo(dialog);
			if (htmlallow) {
				content.html(message);
			} else {
				if (message === undefined || message === null || typeof(message) === 'number') {
					message = new String(message);
				}
				var cs = message.split('\n');
				if (cs.length < 2)
					content.text(cs);
				else {
					$.each(cs, function(i, v) {
						content.append($('<span style="display:block"/>').text(v));
					})
				}
			}

			var btnbar = $("<div class='btnbar'></div>").appendTo(dialog);
			$('<a class="mobile js_ok" href="javascript:;">' + this.lang('message.ok') + '</a>').appendTo(btnbar).on('click', close);

			return dialog;
		},

		confirm2 : function(message, success, fail, options) {
			this.confirm(message, success, fail, options, true);
		},

		/**
		 * 提示应答消息对话框
		 * @method confirm
		 * @description
		 * @param {string} message 消息内容
		 * @param [success] {callback} 确认时执行的回调
		 * @param [fail] {callback} [fail] 取消时执行的回调
		 * @param [options] {object} 入參
		 * @param [htmlallow] {boolean} 是否允许html片段
		 * @return {boolean}
		 */
		confirm : function(message, success, fail, options = {}, htmlallow) {
			var a = this, dialog;
			if (window.$E && $E.device === 'mobile')
				dialog = this.createMobileMessageDialog((window.$E && $E.name) || 'EliteCRM', message, options.htmlallow || htmlallow);
			else
				dialog = this.createMessageDialog((window.$E && $E.name) || 'EliteCRM', message, 4, options.htmlallow || htmlallow);

			var btnbar = $(".btnbar", dialog).addClass('confirm');
			if (window.$E && $E.device === 'mobile') {
				$(">a.js_ok", btnbar).off('click').on('click', function(e) {
					Helper.masker.hide(dialog);
					dialog.remove();
					if ($.isFunction(success))
						success();
				});
				$('<a class="mobile js_cancel" href="javascript:;">' + a.lang('message.cancel') + '</a>').appendTo(btnbar).click(function(e) {
					Helper.masker.hide(dialog);
					dialog.remove();
					if ($.isFunction(fail))
						fail();
				});
			} else {
				$(">button.js_ok", btnbar).off('click').on('click', function(e) {
					var c = $('.btnbar input.js_extchk', dialog).prop('checked');
					Helper.masker.hide(dialog);
					dialog.remove();
					if ($.isFunction(success))
						success(c);
				});
				$('<button class="' + (a.isMobileDevice() ? 'mobile' : 'chrome') + ' js_cancel">' + a.lang('message.cancel') + '</button>').appendTo(btnbar).click(function(e) {
					var c = $('.btnbar input.js_extchk', dialog).prop('checked');
					Helper.masker.hide(dialog);
					dialog.remove();
					if ($.isFunction(fail))
						fail(c);
				});
			}
			if (options.okBtn || options.okTip)
				$(">button.js_ok", btnbar).text(options.okBtn || options.okTip);
			if (options.cancelBtn || options.cancelTip)
				$(">button.js_cancel", btnbar).text(options.cancelBtn || options.cancelTip);
			if (options.onClose)
				$('.close_icon', dialog).on('click', options.onClose);

			dialog.on('keydown', function(e) {
				if (e.keyCode === 27) {
					Helper.masker.hide(dialog);
				}
			}).on('click', function(e) {
				e.stopPropagation();
			});

			Helper.masker.show(dialog, {
				onOpen : function() {
					if (window.$E && $E.device !== 'mobile' && options.chkBtn) {
						$('<label style="float:left"><input type="checkbox" class="js_extchk" /> ' + options.chkBtn + '</label>').appendTo($('.btnbar', dialog));
					}
					$('.js_ok', dialog).focus();
				},
				onClose: function() {
					dialog.off('click');
				}
			});
		},

		/**
		 * 创建对话框
		 * @method createDialog
		 * @param dom {dom} 对话框内容DOM对象
		 * @param [options] {object} 对话框选项
		 *  [options.title] {string} 对话框标题
		 *  [options.escapable] {boolean} 对话框是否允许捕捉'ESC'按键
		 *  [options.masker] {object} 全局遮布选项，等同于masker dom style
		 *  [options.preOpen] {callback} 当初始化对话框时的回调
		 * 	preOpen: function() {
		 * 		// TODO: 进行初始化操作，对话框尚未显示
		 * 	}
		 *
		 *  [options.onOpen] {callback} 当打开对话框时的回调
		 * 	onOpen: function() {
		 * 		// TODO: 对话框已经打开并显示
		 * 	}
		 *
		 *  [options.onSubmit] {callback} 当'确定'按钮按下时的回调
		 * 	onSubmit: function(close) {
		 * 		// TODO: 进行'确定'操作，操作完毕执行close方法关闭对话框
		 * 		if (canClose...)
		 * 			close();
		 * 	}
		 *
		 *  [options.onCancel] {callback} 当'取消'按钮按下时的回调
		 * 	onCancel: function(close) {
		 * 		// TODO: 进行'取消'操作，操作完毕执行close方法关闭对话框
		 * 		close();
		 * 	}
		 *
		 *  [options.onClose] {callback} 当对话框关闭时的回调，对话框已经隐藏
		 * @return {dom} 对话框DOM对象
		 */
		createDialog : function(dom, options) {
			var a = this;
			options = $.extend({
				title : a.lang('message.dialog'),
				closable : true,
				escapable : true,
				removable : false,
				tabIndex : 1,
				masker : {},
				contentCSS : {
					'padding' : '10px',
					'overflow' : 'auto'
				},
				preOpen : false,
				onOpen : false,
				onSubmit : false
				//onCancel: false,
				//onClose: false
			}, options)
			//console.log(options)

			var dialog = $('<div class="exDialog none ' + (options.skin || '') + '"/>');
			if (window.$E && $E.device === 'mobile')
				dialog.addClass('mobile');

			dialog.append('<div class="boxHeader"><div class="topIcon"></div><div class="exTitle"><span></span></div></div>');
			if (options.closable) {
				dialog.append('<a href="javascript:;" class="close_tip_icon" title="' + a.lang('message.close') + '"><i class="far fa-times"></i></a>');
			}
			if (options.icon) {
				if ($.type(options.icon) === 'object')
					$('.boxHeader>.topIcon', dialog).html(options.icon);
				else
					$('.boxHeader>.topIcon', dialog).html('<i class="fa-fw ' + a.fas(options.icon) + '"/>');
			}
			var form = $('<form autocomplete="off" />').appendTo(dialog),
				dialogContent = $('<div class="dialogContent" />').append(dom).appendTo(form).css(options.contentCSS),
				btns = $('<div class="btnsBar" />').appendTo(form).hide(),
				okBtn,
				cancelBtn;
			if ($.isFunction(options.onSubmit)) {
				btns.append($('<button type="submit" class="' + (a.isMobileDevice() ? 'mobile' : 'chrome') + ' js_submit" />').text(options.okTip || a.lang('message.ok')));
				okBtn = true;
			}
			if ($.isFunction(options.onCancel)) {
				btns.append($('<button type="button" class="' + (a.isMobileDevice() ? 'mobile' : 'chrome') + ' js_close js_cancel"/>').text(options.cancelTip || a.lang('message.cancel')));
				cancelBtn = true;
			}
			if (okBtn || cancelBtn)
				btns.show();
			dialog.appendTo($('body'));

			if ($.isFunction(options.preOpen))
				options.preOpen.call(dialog);
			a.dialogOpen(dialog, options);
			return dialog;
		},

		/**
		 * 打开exDialog规范的对话框
		 * @method dialogOpen
		 * @param dialog {dom} 对话框DOM
		 * @param [options] {object} 对话框选项
		 *  [options.escapable] {boolean} 对话框是否允许捕捉'ESC'按键
		 *  [options.masker] {object} 全局遮布选项，等同于masker dom style
		 *  [options.onSubmit] {callback} 当'确定'按钮按下时的回调
		 * 	onSubmit: function(close) {
		 * 		// 进行'确定'操作，操作完毕执行close方法关闭对话框
		 * 		if (canClose...)
		 * 			close();
		 * 	}
		 *
		 *  [options.onCancel] {callback} 当'取消'按钮按下时的回调
		 * 	onCancel: function(close) {
		 * 		// 进行'取消'操作，操作完毕执行close方法关闭对话框
		 * 		close();
		 * 	}
		 *
		 *  [options.onClose] {callback} 当对话框关闭时的回调
		 */
		dialogOpen : function(dialog, options) {
			if (dialog.is(':visible'))
				return this.err('$F - dialogOpen', '重复打开对话框');

			options = $.extend({
				escapable : false,
				tabIndex : 1,
				masker : {},
				removable : false,
				onSubmit : false,
				onCancel : function(close) {
					close()
				},
				onClose : false,
				onOpen : false
			}, options);

			var a = this;
			if (options.title)
				$('.exTitle>span', dialog).text(options.title);

			// make resizable if allow
			if (options.contentCSS)
				$('.dialogContent', dialog).css(options.contentCSS);
			if (options.width)
				$('.dialogContent', dialog).css('width', options.width);
			if (options.height)
				$('.dialogContent', dialog).css('height', options.height);
			if (options.resizable) {
				if (options.resizable.cookie) {
					var size = $.cookie(options.resizable.cookie);
					if (size) {
						size = size.split(',')
						if (size.length == 2) {
							$('.dialogContent', dialog).css({
								width : size[0] + 'px',
								height : size[1] + 'px'
							})
						}
					}
				}
				$('.dialogContent', dialog).resizable({
					minHeight : options.resizable.minHeight || 10,
					minWidth : options.resizable.minWidth || 10,
					stop : function(event, ui) {
						$.cookie(options.resizable.cookie, ui.size.width + ',' + ui.size.height, {
							expires : 30
						});
					}
				})
			}
			dialog.data('_internal_function_onClose', options.onClose);
			var form = $('>form', dialog);
			if (form.length > 0) {
				//console.log('reset form submit');
				form.off('submit').on('submit', function(e) {
					e.preventDefault();
					if ($.isFunction(options.onSubmit)) {
						$F.debounce(options.onSubmit).call(dialog, function() {
							a.dialogClose(dialog);
							if (options.removable)
								dialog.remove();
						});
					}
					return false;
				})
			} else {
				var submitFunction = dialog.data('submitFunction');
				if ($.isFunction(submitFunction)) {
					$("button.js_submit", dialog).off("click", submitFunction);
					dialog.removeData('submitFunction')
				}

				if ($.isFunction(options.onSubmit)) {
					var o = function() {
						$F.debounce(options.onSubmit).call(dialog, function() {
							a.dialogClose(dialog);
							if (options.removable)
								dialog.remove();
						});
					};
					dialog.data('submitFunction', o);
					$("button.js_submit", dialog).on('click', o);
				}
			}

			if ($.isFunction(options.onCancel)) {
				$("a.close_tip_icon", dialog).off("click").on('click', function() {
					$F.debounce(options.onCancel).call(dialog, function() {
						a.dialogClose(dialog);
						if (options.removable)
							dialog.remove();
					})
				});
				$("button.js_close", dialog).off("click").on('click', function() {
					$F.debounce(options.onCancel).call(dialog, function() {
						a.dialogClose(dialog);
						if (options.removable)
							dialog.remove();
					});
				});
			} else {
				$("a.close_tip_icon", dialog).off("click").on('click', function() {
					a.dialogClose(dialog);
					if (options.removable)
						dialog.remove();
				});
			}

			dialog.attr({
				tabIndex : options.tabIndex
			}).css({
				// 'box-shadow': "1px 3px 10px #666"
			});
			if (options.escapable) {
				dialog.off('keydown').on('keydown', function(e) {
					if (e.keyCode == 27) {
						e.preventDefault();
						e.stopPropagation();

						a.dialogClose(dialog);
					}
				})
			}
			if (dialog.draggable) {
				$('.boxHeader', dialog).forceClass('handle');
				$('.btnsBar', dialog).forceClass('handle');
				dialog = dialog.draggable({
					handle : ".handle",
					iframeFix : true,
					containment : "window"
				})
			}

			if ($('.mask', dialog)) {
				$('.mask', dialog).remove();
			}
			if (options.readonly) {
				$('<div class="mask" style="width: 100%;height: calc(100% - 50px);background: rgba(0,0,0,.1);position: absolute;left: 0;top: 51px"></div>').appendTo(dialog);
			} else {
				if (options.virtualNodeReadonly) {
					$('<div class="mask" style="width: 100%;height: calc(100% - 180px);background: rgba(0,0,0,.1);position: absolute;left: 0;top: 120px"></div>').appendTo(dialog);
				} else {
					if (options.contentReadonly) {
						$('<div class="mask" style="width: 100%;height: calc(100% - 180px);background: rgba(0,0,0,.1);position: absolute;left: 0;top: 120px"></div>').appendTo(dialog);
					}
				}
			}

			if (options.hideIcon) {
				$('.topIcon ', dialog).hide()
			}

			Helper.masker.show(dialog, {
				onOpen : function() {
					dialog.focus();
					if ($.isFunction(options.onOpen))
						options.onOpen.call(dialog);
				},
				full: options.full,
				container: options.container,
				zIndex: options.zIndex,
				skin: options.skin
			});
		},

		/**
		 * 关闭exDialog规范的对话框
		 * @method dialogClose
		 * @param dialog {dom} 对话框DOM
		 */
		dialogClose : function(dialog) {
			dialog = $(dialog);
			$('body').unbind('keydown', dialog.data('_internal_function_onEscape'));
			Helper.masker.hide(dialog);
			dialog.off('click');
			var onClose = dialog.data('_internal_function_onClose');
			if ($.isFunction(onClose)) {
				onClose.call(dialog);
			}
		},

		/**
		 * 判断某个对象是否为undefined或者null
		 *
		 * @method isNull
		 * @param e {any} 对象
		 * @return {any}
		 */
		isNull : function(e) {
			return (e == undefined || e == null);
		},

		clone: function(obj) {
			// Handle the 3 simple types, and null or undefined
			if (null == obj || "object" != typeof obj) return obj;

			// Handle Date
			if (obj instanceof Date) {
				var copy = new Date();
				copy.setTime(obj.getTime());
				return copy;
			}

			// Handle Array
			if (obj instanceof Array) {
				var copy = [];
				for (var i = 0; i<obj.length; i++) {
					copy[i] = this.clone(obj[i]);
				}
				return copy;
			}

			// Handle Object
			if (obj instanceof Object) {
				var copy = {};
				for (var attr in obj) {
					if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
				}
				return copy;
			}

			throw new Error("Unable to copy obj! Its type isn't supported.");
		},

		copy : function(objA, objB, cover, exclude) {
			if (objB == undefined || objB == null)
				return objA

			for (var key in objB) {
				if (exclude && exclude.contains(key.toLowerCase()))
					continue;

				if (!objB.hasOwnProperty(key))
					continue;
				if (objA.hasOwnProperty(key) && !cover)
					continue;
				objA[key] = objB[key];
			}
			return objA;
		},


		/**
		 * 判断某个对象是否为undefined或者null或者空字符串
		 * @method isEmpty
		 * @param e {any} 对象
		 * @return {boolean}
		 */
		isEmpty : function(e) {
			return (e == undefined || e == null || (typeof (e) == "string" && e.trim() == ""));
		},

		/**
		 * 判断输入字符串是否是一个颜色字符串 #FFFFFF
		 * @method isColorStr
		 * @param e {string} 对象
		 * @return {boolean}
		 */
		isColorStr : function(e) {
			var reg = /^#?[0-9|a-f|A-F]{6}$/;
			return reg.test(e);
		},

		/**
		 * 判断一个文件名后缀是不是属于图片类型
		 * @param fileName
		 */
		isImage : function(fileName) {
			if (fileName.lastIndexOf('.') > -1) {
				fileName = fileName.substring(fileName.lastIndexOf('.') + 1);
			}
			var exts = [ 'PNG', 'JPG', 'JPEG', 'BMP', "GIF" ];
			for (var i = 0; i < exts.length; i++) {
				var ext = exts[i];
				if ($F.equalsIgnoreCase(fileName, ext)) {
					return true;
				}
			}
			return false;
		},

		/**
		 * 忽略大小写进行比较
		 * @param s {string} 比较源
		 * @param t {string} 比较目标
		 * @return {boolean}
		 */
		equalsIgnoreCase : function(s, t) {
			if (this.isEmpty(s) && this.isEmpty(t)) {
				return true;
			}
			if (!this.isEmpty(s) && !this.isEmpty(t) && s.toLowerCase() == t.toLowerCase()) {
				return true;
			}
			return false;
		},

		equalValue : function(a, b) {
			if ($.type(a) == 'number')
				a = a + '';
			if ($.type(b) == 'number')
				b = b + '';
			return a === b;
		},


		/**
		 * 判断如果入参是null或者undefined或者空字符串,则返回空字符串，不然返回本身
		 * @method parseEmpty
		 * @param str {any} 入参对象
		 * @return {any}
		 */
		parseEmpty : function(str) {
			return this.isEmpty(str)? '' : str.toString();
		},

		/**
		 * 字符串去空格，如果传入字符串为undefined或者null，返回空字符串
		 * @method trimToEmpty
		 * @param str {any} 入参对象
		 * @return {any}
		 */
		trimToEmpty : function(str) {
			return $.type(str) == "string"? str.trim() : '';
		},

		/**
		 * 替换字符串中所有符合正则的字符，忽略大消息
		 * @method replaceIngoreCase
		 * @param s 原始字符串
		 * @param t 需要替换的字符串
		 * @param r 替换成的内容
		 * @return {string} 返回替换后字符
		 */
		replaceIngoreCase : function(s, t, r) {
			if (s) {
				return s.replace(new RegExp(t, "gi"), r);
			}
			return s;
		},

		/**
		 * 提供OGNL变量模板替换
		 * @param expression {string} 模板字符串
		 * @param callback {function|object} 变量回调函数或数据对象
		 * @param turnback {boolean} 是否在ognl操作完成后将@{xxx}传译为${xxx}
		 * @example
		 * 		var values = {
		 * 			name: 'Kevin',
		 * 			city: 'Shanghai'
		 * 		}
		 * 		var exp = 'My name is ${name}, i am living in ${city}';
		 * 		exp = $F.ognl(exp, function(varName){
		 * 			return values[varName];
		 * 		})
		 * 		console.log(exp)
		 *
		 * return {string}
		 */
		ognl : function(expression, callback, turnback) {
			if (!expression)
				return '';

			// console.log("OGNL: " + expression)
			var matches = expression.match(/\$\{([\u4e00-\u9fa5 | \w | \. | \- | @ | %]+)\}/gi),
				ex,
				v;
			// console.log('matches: ' + matches);
			if (matches) {
				for (var i = 0; i < matches.length; i++) {
					expression = expression.replace(matches[i], function() {
						//console.log('match: ' + matches[i])
						ex = (/\$\{([\u4e00-\u9fa5 | \w | \. | \- | @ | %]+)\}/gi).exec(matches[i]);
						// console.log('exec: ' + ex);
						ex[1] = ex[1].trim();
						if ($.isFunction(callback)) {
							v = callback(ex[1]);
							if (v === undefined)
								return matches[i];
							else
								return v;
						} else {
							if (callback && callback.hasOwnProperty(ex[1]))
								return callback[ex[1]];
							else
								return matches[i];
						}
					})
				}
			}
			if (turnback)
				return this.ognl2(expression);
			return expression;
		},

		/**
		 * 将@{xxx}格式的隐形变量申明转为明文${xxx}格式
		 */
		ognl2 : function(expression) {
			if (!expression)
				return '';

			var matches = expression.match(/@\{([\u4e00-\u9fa5 | \w | \. | \- | @ | %]+)\}/gi);
			if (matches) {
				for (var i = 0; i < matches.length; i++) {
					expression = expression.replace(matches[i], function() {
						//console.log('match: ' + matches[i])
						var ex = (/@\{([\u4e00-\u9fa5 | \w | \. | \- | @ | %]+)\}/gi).exec(matches[i]);
						return '${' + ex[1] + '}';
					})
				}
			}
			return expression;
		},

		/**
		 * 获取服务端当前时间 yyyy-MM-dd HH:mm:ss
		 * @method getDbTime
		 * @return {string}
		 */
		getDbTime : function() {
			this.localTimeSync(); // 先sync一次，避免时间往前改造成setInterval本身不执行了
			if (dbtimeObject) {
				return new Date(Date.now() - dbtimeObject.timestamp + dbtimeObject.serverTime).format('yyyy-MM-dd hh:mm:ss');
			} else {
				clearInterval(localTimeSyncTimer);
				var result = this.ajaxSync("GetDBTime", {});
				if (result && result.code > 0) {
					var nowString = result.value;
					dbtimeObject = {
						serverTime : this.parseDate(nowString).getTime(),
						timestamp : Date.now()
					};
					// 启动定时检查机制，避免本地时间被修改后，获取的时间都是错误的时间
					localTimeSyncTimer = setInterval(function() {
						$F.localTimeSync();
					}, localTimeSyncInterval);
					return nowString;
				}
				this.err("$F - getDbTime", "获取数据库时间失败, " + (result ? result.message : '请求异常'));
				var now = new Date();
				dbtimeObject = {
					serverTime : now.getTime(),
					timestamp : now.getTime()
				};
				return now.format('yyyy-MM-dd hh:mm:ss');
			}
		},

		localTimeSync: function() {
			if (!localTime || !localTimezone) {
				localTime = new Date();
				localTimezone = localTime.getTimezoneOffset();
			}
			let now = new Date(),
				elapse = now.getTime() - localTime.getTime();
			if (elapse < 0 || elapse > 5 * localTimeSyncInterval || localTimezone !== now.getTimezoneOffset()) {
				dbtimeObject = null; // 清空数据库时间缓存，下次获取数据库时间需要重新获取
				localTimezone = now.getTimezoneOffset();
			}
			localTime = now;
		},

		/**
		 * 判断字符串是否符合日期格式
		 * @method isDate
		 * @param val {string|number} 日期字符串
		 * @return {boolean}
		 */
		isDate : function(val) {
			if (!val) {
				return false;
			}
			//在ie下，如果val是2016-02-01 13:47:57  new Date出来的valueOf是NaN，这里新增了这个parseDate方法用来识别日期
			var ret = false,
				d = this.parseDate(val);
			if (d) {
				ret = !isNaN(d.valueOf());
			}
			return ret;
		},

		parseDate : function(t) {
			// 日期格式化
			if (t === '')
				return new Date();

			if ($.type(t) === 'date')
				return t;

			if ($.type(t) === 'number')
				return new Date(t);

			if ($.type(t) !== 'string') {
				return new Date();
			}

			let st = t.split(' '), format;
			if (st.length < 2) {
				let subt = st[0].split(':');
				if (subt.length > 1) {
					// 字符串为时间
					if (subt.length === 2)
						format = "H:i";
					else
						format = "H:i:s";
				} else {
					// 字符串为日期
					let x = subt[0].split('-').length;
					if (x > 2)
						format = "Y-m-d";
					else if (x > 1)
						format = 'Y-m';
					else
						format = 'Y';
				}
			} else {
				// 字符串为完整日期时间
				let subt = t.trim().split(':');
				if (subt.length > 1) {
					// 字符串为时间
					if (subt.length === 2)
						format = "Y-m-d H:i";
					else {
						if (t.match(/^(\d{4})-(\d{1,2})-(\d{1,2}) (\d{1,2}):(\d{2}):(\d{2}).(\d{1,3})$/))
							format = Date.patterns.ISO8601LongestPattern;
						else
							format = Date.patterns.ISO8601LongPattern;
					}
				} else {
					// 字符串为日期
					format = "Y-m-d H";
				}
			}
			// console.info(format);
			return Date.parseDate(t, format);
		},

		/**
		 * 日期字符串化转为yyyy-MM-dd标准格式
		 */
		formatDate : function(t, format) {
			// 日期格式化转为yyyy-MM-dd标准格式
			if (!t)
				return '';
			if (!format)
				return t;

			var date = Date.parseDate(t, format);
			if (!date)
				return t;
			var outFormat = '';
			if (/Y/.test(format))
				outFormat += 'yyyy';
			if (/m/.test(format))
				outFormat += (outFormat ? '-' : '') + 'MM';
			if (/d/.test(format))
				outFormat += (outFormat ? '-' : '') + 'dd';
			if (/H/.test(format))
				outFormat += (outFormat ? ' ' : '') + 'HH';
			if (/i/.test(format))
				outFormat += (outFormat ? ':' : '') + 'mm';
			if (/s/.test(format))
				outFormat += (outFormat ? ':' : '') + 'ss';
			return date.format(outFormat);
		},

		/**
		 * 毫秒数转化为 hh: mm: ss
		 */
		formatDuration: function (duration) {
			const millies = parseInt(duration),				// 毫秒
				seconds = parseInt(millies / 1000),		// 秒
				minutes = parseInt(seconds / 60),			// 分
				secondsMod = parseInt(seconds % 60),			// 余秒
				hours = parseInt(minutes / 60),		// 小时
				minutesMod = parseInt(minutes % 60);			// 余分

			const hoursStr = hours > 9 ? hours : ('0' + hours),
				minutesStr = minutesMod > 9 ? minutesMod : ('0' + minutesMod),
				secondsStr = secondsMod > 9 ? secondsMod : ('0' + secondsMod);
			return hoursStr + ':' + minutesStr + ':' + secondsStr;
		},

		isSameDay : function(date1, date2) {
			return date1.getYear() == date2.getYear()
				&& date1.getMonth() == date2.getMonth()
				&& date1.getDay() == date2.getDay();
		},

		betweenDays : function(date1, date2) {
			var d1 = new Date(date1.getTime());
			d1.setHours(0, 0, 0, 0);
			var d2 = new Date(date2.getTime());
			d2.setHours(0, 0, 0, 0);
			// console.info((d2.getTime() - d1.getTime()))
			return Math.floor((d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24));
		},

		/**
		 * 按单位修改时间
		 * @method dateAdd
		 * @param date {string} 需要修改的时间对象
		 * @param c {string} 单位
		 * @param v {integer} 修改的值
		 * @return {date}
		 */
		dateAdd : function(date, c, v) {
			if (!(date instanceof Date)) {
				date = $F.parseDate(date);
			}
			if (c == "day") {
				return new Date(date.getTime() + v * 24 * 60 * 60 * 1000);
			}
			if (c == "hour") {
				return new Date(date.getTime() + v * 60 * 60 * 1000);
			}
			if (c == "minute") {
				return new Date(date.getTime() + v * 60 * 1000);
			}
			if (c == "second") {
				return new Date(date.getTime() + v * 1000);
			}
		},

		/**
		 * 获得下一日
		 * @method getNextEndDate
		 * @param date {date|string} 指定日期
		 * @param c {string} 类型
		 * @param v {integer} 值
		 * @param fd {array} 所有休息日的数组
		 * @param [wh] {string} 工作小时时段,示例: 08:00-11:30,13:00-18:00
		 * @return {string}
		 */
		getNextEndDate : function(date, c, v, fd, wh) {
			try {
				if (!(date instanceof Date)) {
					date = $F.parseDate(date);
				}
				if (c === "D") { // 天
					date = new Date(date.getTime() + v * 24 * 60 * 60 * 1000);
					return date.format();
				} else if (c === "E") { // 工作日
					if (fd) {
						if (v > 0) {
							for (let i = 0; i < v; i++) {
								if (!$F.isNull(fd[(new Date(date.getTime() + i * 24
									* 60 * 60 * 1000)).format('YYYY-MM-dd')])) {
									v++;
								}
							}
						} else {
							for (let i = 0; i > v; i--) {
								if (!$F.isNull(fd[(new Date(date.getTime() + i * 24
									* 60 * 60 * 1000)).format('YYYY-MM-dd')])) {
									v--;
								}
							}
						}
					}
					date = new Date(date.getTime() + v * 24 * 60 * 60 * 1000);
					return date.format();
				} else if (c === "H") { // 小时
					date = new Date(date.getTime() + v * 60 * 60 * 1000);
					return date.format();
				} else if (c === "M") { // 分钟
					date = new Date(date.getTime() + v * 60 * 1000);
					return date.format();
				} else if (c === "WH") { // 工作小时
					// 2021-12-19 14:00:00
					// $F.getNextEndDate("2021-12-14 00:08:00", "WH", 30, {"2021-12-14":1, "2021-12-15": 1}, "08:00-11:30,13:00-18:00")
					// 2021-12-10 16:30:00
					// $F.getNextEndDate("2021-12-14 00:08:00", "WH", -10, {"2021-12-12":1, "2021-12-11": 1}, "08:00-11:30,13:00-18:00")
					if (wh) {
						// 生成某天的工作时间数组
						const whsCache = {};
						const genWhs = function(date) {
							let dateStr = date.format("yyyy-MM-dd");
							if (whsCache[dateStr]) {
								return whsCache[dateStr];
							} else {
								let whs = [];
								wh.split(",").forEach(owh => {
									whs.push({
										startTime: new Date(dateStr + " " + owh.split("-")[0]).getTime(),
										endTime: new Date(dateStr + " " + owh.split("-")[1]).getTime()
									});
								});
								whsCache[dateStr] = whs;
								return whs;
							}
						};

						const isInWorkingHour = function(date, negative) {
							const whs = genWhs(date);
							let isWh = false;
							whs.some(wh => {
								let dateTime = date.getTime();
								if (negative) {
									if (dateTime > wh.startTime && dateTime <= wh.endTime) {
										isWh = true;
										return true;
									}
								} else {
									if (dateTime >= wh.startTime && dateTime < wh.endTime) {
										isWh = true;
										return true;
									}
								}
							});
							return isWh;
						};

						const isWorkingDay = function(date) {
							if (fd) {
								return $F.isNull(fd[date.format('YYYY-MM-dd')]);
							}
							return true;
						};

						// 换算成需要往后推几分钟
						let minutes = v * 60;
						if (v > 0) {
							// 循环需要推的分钟数
							for (let i = 0; i < minutes; i++) {
								// 跳过休息日天
								while (!isWorkingDay(date)) {
									date.setDate(date.getDate() + 1);
									date.setHours(0);
									date.setMinutes(0);
									date.setSeconds(0);
								}
								// 判断是不是在工作时间，如果是就过，如果不是工作时间，则需要往后推的分钟数+1
								if (!isInWorkingHour(date, false)) {
									minutes++;
								}
								//  +1 minute 往后推1分钟
								date = new Date(date.getTime() +  60 * 1000);
							}
						} else {
							// 如果是负数，则往前减
							for (let i = 0; i > minutes; i--) {
								if (!isWorkingDay(date)) {
									while (!isWorkingDay(date)) {
										date.setDate(date.getDate() - 1);
									}
									date.setDate(date.getDate() + 1);
									date.setHours(0);
									date.setMinutes(0);
									date.setSeconds(0);
								}

								if (!isInWorkingHour(date, true)) {
									minutes--;
								}
								date = new Date(date.getTime() -  60 * 1000);
							}
						}
					} else {
						date = new Date(date.getTime() + v * 60 * 60 * 1000);
					}
					return date.format();
				} else {
					return date.format();
				}
			} catch (e) {
				console.error('getNextEndDate error: ', e);
			}
			return '';
		},

		dateAddFormat : function(date, c, v) {
			return this.dateAdd(date, c, v).format();
		},

		getDay : function(date) {
			return a.lang('sys.days.' + date.getDay());
		},

		/**
		 * 对某个基准数按照上下限取值并生成二维数组（标准表格型变量)
		 * @param number {integer} 基准数字
		 * @param negative {integer} 最小值下限
		 * @param positive {integer} 最大值上限
		 * @param [step] {integer} 跃进值
		 */
		getNumberOptions : function(number, negative, positive, step) {
			var min = number;
			var max = number;
			var cs = [number];
			if (step === undefined || step <= 0)
				step = 1;
			while(min-step >= negative) {
				min -= step;
				cs.unshift(min);
			}
			while(max+step <= positive) {
				max += step;
				cs.push(max);
			}
			var array3 = [];
			cs.forEach(function(x, i) {
				array3.push([x, x])
			})
			return array3;
		},

		fireValue : function(instance, propertyName, propertyValue) {
			if (instance) {
				instance.fireValue(propertyName, instance.runtime, {
					type : 0,
					data : propertyValue
				});
			}
		},

		/**
		 * 给url替换变量
		 */
		addArgumentsToURL : function(url, proj) {
			var resultURL = url.replace(/:staff_id/gi, $E.staff.id)
				.replace(/:agent_id/gi, $E.staff.agentId)
				.replace(/:loginname/gi, $E.staff.loginName)
				.replace(/:staffname/gi, encodeURIComponent($E.staff.name))
				.replace(/:issupervisor/gi, $E.staff.isSupervisor)
				.replace(/:rolegroup_id/gi, proj.groupId)
				.replace(/:token/gi, proj.token)
				.replace(/:ds/gi, $E.dataService.key)
				.replace(/:lang/gi, $E.lang);

			if (proj.cwInfo && proj.cwInfo.callInfo) {
				resultURL = resultURL.replace(/:ani/gi, proj.cwInfo.callInfo.getValue('ani') || '')
					.replace(/:dnis/gi, proj.cwInfo.callInfo.getValue('dnis') || '')
					.replace(/:extension/gi, proj.cwInfo.callInfo.getValue('extension') || '')
					.replace(/:event_guid/gi, proj.cwInfo.callInfo.eventGuid);
			}

			if (proj.customer) {
				resultURL = resultURL.replace(/:customer_guid/gi, proj.customer.getValue("customer_guid"))
					.replace(/:customername/gi, encodeURIComponent(proj.customer.getValue("customername")));
			}

			var staff = proj.getStaffById($E.staff.id);
			if (url.indexOf("?") != -1) {
				var argsStr = url.substr(url.indexOf("?") + 1);
				var args = argsStr.split("&");
				$.each(args, function(i, arg) {
					var argV = arg.substr(arg.indexOf("=") + 1);
					if (argV.startWith(':staff.')) {
						var staffKey = argV.replace(':staff.', '');
						resultURL = resultURL.replace(argV, encodeURIComponent(staff.bean[staffKey.toUpperCase()]));
					}
				});
			}
			return resultURL;
		},

		/**
		 * 创建单条SQL执行的JSON对象
		 *
		 *	// 该方法主要用于SystemDo2数据接口中进行SQL操作的快速封装
		 *	// sql请求数据封装完成后的结构是：
		 *	data : {
		 *		token: {string},
		 *		sqls: [
		 *			{
		 *				key: {string},
		 *				dbPool: {string},
		 *				params: {
		 *					C0: {any},
		 *					C1: {any},
		 *					C2: {any},
		 *					...
		 *				},
		 *				maxRow: {integer},
		 *				page: {integer}
		 *		]
		 *	}
		 *	// 请求方法实例：
		 *	var params = ['参数1', '参数2', '参数3', ...];
		 *	var sqlKey = 'SQL_KEY';
		 *	var sqlBean = $F.sqlbean(sqlKey, token, params);	// 使用系统默认最大行数，并且读取第1页（下标为0）
		 *	$F.dataService('SystemDo2', {
		 *		data: sqlBean,
		 *		success: function(data) {
		 *			// TODO: 处理数据，data的结构为标准XHR请求Result结构
		 *		},
		 *		fail: function(data) {
		 *		}
		 *	})
		 *
		 * @method sqlbean
		 * @param key {string|object} 数据服务方法名或者对象，为对象时，key为Area<br/>
		 *  {'MSG': 'load.outbox'} or 'Msg.load.outbox'
		 * @param token {string} 项目登录凭据
		 * @param [params] {array} 数据服务的传参队列
		 * @param [max] {int} 分页最大行数
		 * @param [page] {int} 当前页码
		 * @return {object} 数据服务请求的入参对象
		 */
		sqlbean : function(key, token, params, max, page) {
			return this.sqlbean2(key, token, '', params, max, page);
		},

		sqlbean2 : function(key, token, dbPool, params, max, page) {
			var areaKey = '';
			if ($.type(key) == 'object') {
				areaKey = Object.keys(key)[0];
				key = key[areaKey];
			}
			var dataRequest = {
				token : token,
				dbPool : dbPool,
				sqls : []
			};
			var map = {};
			$.each(params, function(i, v) {
				map['C' + (i + 1)] = v;
			});
			var sql = {
				areaKey: areaKey,
				key : key,
				params : map
			};
			if (max)
				sql.maxRow = max;
			if (page)
				sql.page = page;
			dataRequest.sqls.push(sql);
			return dataRequest;
		},

		/**
		 * 创建请求树形结构数据的SQL执行的JSON对象
		 *
		 *	// 该方法主要用于SystemDo4（树形数据读取接口）中进行SQL操作的快速封装
		 *	// sql请求数据封装完成后的结构是：
		 *	data : {
		 *		token: {string},
		 *		sqls: [
		 *			{
		 *				key: {string},
		 *				params: {
		 *					C0: {any},
		 *					C1: {any},
		 *					C2: {any},
		 *					...
		 *				},
		 *				nodeId: {string},
		 *				parentId: {string}
		 *		]
		 *	}
		 *	// 请求方法实例：
		 *	var params = ['参数1', '参数2', '参数3', ...];
		 *	var sqlKey = 'SQL_KEY';
		 *	var nodeId = 'MENUITEM_GUID';	// 数据库中标识ID的字段名
		 *	var parentId = 'PARENT_GUID';	// 数据库中标识父节点ID的字段名
		 *	var sqlBean = $sqltree(sqlKey, token, params, nodeId, parentId);	// 树形结构数据的读取最大返回行数由系统接口自行决定
		 *	$F.dataService('SystemDo4', {
		 *		data: sqlBean,
		 *		success: function(data) {
		 *			// TODO: 处理数据，data的结构为标准XHR请求Result结构
		 *		},
		 *		fail: function(data) {
		 *		}
		 *	})
		 *
		 * @method sqltree
		 * @param {string} key 数据服务方法名
		 * @param {string} token 项目登录凭据
		 * @param {array} params 数据服务的传参
		 * @param {string} nodeId 数据表中的记录ID字段名
		 * @param {string} parentId 数据表中的记录父ID字段名
		 * @return {map} 数据服务请求的入参对象
		 */
		sqltree : function(key, token, params, nodeId, parentId, mustempty) {
			return this.sqltree2(key, token, '', params, nodeId, parentId, mustempty);
		},

		sqltree2 : function(key, token, dbPool, params, nodeId, parentId, mustempty) {
			var areaKey = '';
			if ($.type(key) == 'object') {
				areaKey = Object.keys(key)[0];
				key = key[areaKey];
			}

			var map = {};
			$.each(params, function(i, v) {
				map['C' + (i + 1)] = v;
			});
			return {
				token : token,
				dbPool : dbPool,
				sql : {
					areaKey: areaKey,
					key : key,
					params : map,
					nodeId : nodeId,
					parentId : parentId,
					mustempty : mustempty
				}
			};
		},

		/**
		 * Ajax异步请求 // 使用示例 $F.ajaxAsync('MYMETHOD', { param1: 'xxx', param2: 100 }, function(data){ if (! $F.isNull(data)) { // 处理返回的数据，数据格式为标准XHR请求Result if (data.code > 0) { ... } else { ... } }
		 * else { // TODO：处理通讯请求失败 } } }
		 *
		 * @method ajaxAsync
		 * @param {string}
		 *            method 数据服务的方法名
		 * @param {object}
		 *            data 入参，接口使用PAYLOAD方式提交数据
		 * @param {callback}
		 *            callback 回调方法
		 */
		ajaxAsync : function(method, data, callback, digest, headers) {
			this.dataService(method, {
				data : data,
				digest: digest,
				headers: headers,
				success : function(ajaxData) {
					callback(ajaxData);
				},
				fail : function() {
					callback();
				},
				error : function() {
					callback();
				},
				silent : true
			});
		},

		/**
		 * Ajax同步请求
		 *
		 * 	// 使用示例
		 * 	var data = $F.ajaxSync('MYMETHOD',
		 * 					{
		 * 						param1: 'xxx',
		 * 						param2: 100
		 * 					});
		 * 	if ($F.isNull(data)) {
		 * 		if (data.code > 0){
		 * 			...
		 * 		} else {
		 * 			...
		 * 		}
		 * 	}
		 *
		 * @method ajaxSync
		 * @param {string} method 数据服务的方法名
		 * @param {object} data 入参，接口使用PAYLOAD方式提交数据
		 * @param {string} digest
		 * @return {any} 数据请求结果JSON
		 */
		ajaxSync : function(method, data, digest, headers) {
			var result;
			this.dataService(method, {
				data : data || {},
				async : false,
				digest: digest,
				headers: headers,
				success : function(ajaxData) {
					result = ajaxData;
				},
				fail : function(xhr) {
					result = {
						code : -1,
						message: xhr.status + " : " + xhr.statusText
					}
				}
			});
			return result;
		},

		/**
		 * 调用系统数据接口方法
		 *
		 * 	// 使用示例
		 * 	$F.process(function() {
		 * 			$F.dataService('MY_METHOD', {
		 *					data: <any>   		// 请求提交的PAYLOAD数据
		 *					async : false,		// 使用同步处理
		 *					success: function(data) {
		 *						// TODO: 处理返回的数据
		 *					}
		 *				});
		 * 		}, '正在请求数据，请稍等...');
		 * 	}
		 *
		 * @method dataService
		 * @param {string} method 接口方法名
		 * @param {object} options 入参的对象，包含data, async, success, fail, error
		 *  {string} [options.data] JSON字符串，使用JSON.stringify(data)进行转义
		 *  {boolean} [options.async] 是否异步执行，默认为true
		 *  {number} [options.timeout] 超时时间设置，单位毫秒
		 *  {callback} [options.success] 执行完毕后的回调
		 *  {callback} [options.fail] 执行失败时的回调
		 *  {callback} [options.error] 执行发生错误时的回调
		 */
		dataService : function(method, opts) {
			opts.encode = $E.payloadEncode;
			var $this = this, sqlKeys = '', threadId = Date.now();

			// console.info('[DataService]', url, opts.data);
			if (method == 'SystemDo2' || method == 'SystemDo1')
				sqlKeys = (opts.data.sqls.length > 1 ? (opts.data.sqls[0].key + '...') : opts.data.sqls[0].key);
			else if (method == 'SystemDo4')
				sqlKeys = opts.data.sql.key;
			else if (method == 'SystemDo3')
				sqlKeys = (opts.data.sqls.length > 1 ? (opts.data.sqls[0] + '...') : opts.data.sqls[0]);
			else if (method == 'JSGenerateServlet4' || method == 'JSGenerateServlet2')
				sqlKeys = opts.data.name;
			else if (method == 'JSGenerateServlet')
				sqlKeys = opts.data.clsName;
			else if (method == 'SysCacheJSON')
				sqlKeys = opts.data.Name;
			else if (method == 'CacheDo2')
				sqlKeys = (opts.data.sqls.length > 0 ? (opts.data.sqls.length > 1 ? (opts.data.sqls[0].cacheId + '...') : opts.data.sqls[0].cacheId) : '');
			else if (method == 'CacheDoPubSql2')
				sqlKeys = (opts.data.sqls.length > 0 ? (opts.data.sqls.length > 1 ? (opts.data.sqls[0].pubKey + '...') : opts.data.sqls[0].pubKey) : '');
			else
				sqlKeys = '';

			// 添加默认模块的id
			if (['SystemDo1', 'SystemDo2', 'SystemDo3', 'SystemDo4', 'SystemDo5'].includes(method)) {
				if (opts.data.sqls) {
					if ('SystemDo3' === method)
						opts.data.dynId = $CONST.ModuleId.COMMON;
					else {
						$.each(opts.data.sqls, function (i, sql) {
							sql.dynId = opts.data.dynId || $CONST.ModuleId.COMMON;
						})
					}
				} else if (opts.data.sql) {
					opts.data.sql.dynId = opts.data.dynId || $CONST.ModuleId.COMMON;
				}
				if ('SystemDo5' === method) {
					opts.data.dynId = $CONST.ModuleId.COMMON;
				}
			}

			if (! opts.quietly && $E.debug) {
				if (H5Utils.isChrome)
					console.info('%c[DataService - ' + threadId + '] ' + method + ' (' + sqlKeys + ')', 'color:#0d6b2b', opts.data)
				else
					console.info('[DataService - ' + threadId + '] ' + method + ' (' + sqlKeys + ')', opts.data)
			}

			// opts.data.lang = $E.lang;
			var data = JSON.stringify(opts.data),
				url = $this.getServiceMethod(method),
				digest = '',
				token = '';

			if (! ['GetDBTime', 'LoginInfo', 'XMLConvert'].contains(method)) {
				token = (method == 'SysCacheJSON' ? opts.data.Key : opts.data.token);
				if (! token) {
					opts.success && opts.success({
						code: -1,
						message: '报文无法签名: ' + method
					});
					return;
				}
				if (Object.keys(opts.data).length > 0) {
					if (opts.digest === undefined)
						$F.error('发现未签名报文: ' + method, opts);
					else
						digest = $this.signature(data, token, opts.digest);
				}
			}
			opts.data.threadId = threadId;

			const key = 'Letmein_03082022';
			if (opts.encode === 'base64')
				data = btoa(data);
			else if (opts.encode === 'des')
				data = this.Security.desEncrypt(data, key);
			else if (opts.encode === 'aes')
				data = this.Security.aesEncrypt(data, key);

			const start = new Date(), busyId = (opts.silent || $E.progressing || $E.silent) ? false : $this.busyShow(opts.title || '');
			Ajax.post({
				url : url,
				data: data,
				contentType : 'application/json;charset=UTF-8',
				async : opts.async,
				timeout: opts.timeout,
				cache : false,
				headers : $.extend(opts.headers || {}, {
					'THREADID' : threadId,
					'TERMINAL' : ($E.admin ? 'admin' : ''),
					'DS': $E.dataService.key,
					'SIGNER': digest,
					'ENCODE': opts.encode
				}),
				success : function(data, textStatus, jqXHR) {
					const t = (new Date() - start);
					if (t > 500 && $E.debug) {
						if (H5Utils.isChrome)
							console.warn('%c[DataService - ' + threadId + '] ' + method + ' (' + sqlKeys + '), took ' + t + '\'ms', "color:#ff0000;font-size: 1.5em");
						else
							console.warn('[DataService - ' + threadId + '] ' + method + ' (' + sqlKeys + '), took ' + t + '\'ms');
					}
					//	console.error(data);
					if (data.code == -50001) {
						window.onbeforeunload = null;
						if ($E.device == 'mobile') {
							$E.reLogin();
							return;
						} else {
							if (! $E.invalidState) {
								$E.invalidState = true;
								var errmsg = $this.lang('sys.tokenExpired');
								$this.err('$F - dataService', errmsg);
								$this.alert(errmsg, function() {
									if ($E.admin)
										location.href = $E.url + "/admin/logout?e=tokenInvalid";
									else
										location.href = $E.url + ($E.query == 'portal' ? '/portal' : '') + '/logout?e=tokenInvalid';
								})
							}
							return;
						}
					} else if (data.code == -50002) {
						window.onbeforeunload = null;
						if ($E.device == 'mobile') {
							$E.reLogin();
							return;
						} else {
							if (!$E.invalidState) {
								$E.invalidState = true;
								var errmsg = $this.lang('sys.sessionExpired');
								$this.err('$F - dataService', data.message);
								$this.alert(data.message, function() {
									if ($E.admin)
										location.href = $E.url + "/admin/logout?e=sessionInvalid";
									else
										location.href = $E.url + ($E.query == 'portal' ? '/portal' : '') + '/logout?e=sessionInvalid';
								})
							}
							return;
						}
					} else if (data.code == -50005) {
						$this.err('$F - dataService', '签名异常，数据请求失败');
						return;
					}
					try {
						data.value = Ajax.d(data.value, jqXHR);
						opts.success && opts.success(data);
						if (opts.successResult) {
							if (data.code < 0)
								$this.notify(data.message, -1);
							else
								opts.successResult(data.value);
						}
					} catch (e) {
						console.error(e)
					}
				},
				fail : function(jqXHR, textStatus, errorThrown) {
					if (jqXHR.status == 200) {
						$this.err('$F - dataService', textStatus);
						//console.log(jqXHR.responseText)
						//console.log(JSON.parse(jqXHR.responseText))
						//console.error(errorThrown);
					} else
						$this.err('$F - dataService', '[STATUS:' + jqXHR.status + ']', textStatus);
					if ($.isFunction(opts.fail)) {
						opts.fail(jqXHR, textStatus, errorThrown);
					}
				},
				always : function() {
					if (busyId)
						$this.busyHide(busyId);
					if ($.isFunction(opts.always)) {
						opts.always();
					}
				}
			})
		},

		/*
		 * 从配置中获得系统数据服务地址
		 */
		getServiceMethod : function(method) {
			if ($E.dataService.api) {
				return $E.url + '/proxy?method=' + encodeURIComponent(method);
			} else {
				// 直接访问同一个JVM下的其他WebApp服务
				return $E.dataService.url + '/' + method;
			}
		},

		/**
		 * 调用ngs的http接口方法，默认采用签名模式
		 * @param options {object} 常规的Ajax入参
		 * @param [token] {string} 调用者凭据，无此入参时从options.data中获取token
		 * @param [digest] {integer} 接口签名标志位，无此入参时从options.data中获取digest
		 */
		ngsService : function(options, token, digest) {
			options = $.extend({
				contentType : 'application/json;charset=UTF-8',
				async : true,
				cache : false,
				silent: false
			}, options)
			var a = this,
				threadId = Date.now(),
				busyId = (options.silent || $E.progressing || $E.silent ? '' : a.busyShow(options.message || '')),
				action = (options.type == 'get' ? Ajax.get : Ajax.post);

			if (arguments.length < 3)
				digest = options.data.digest;
			if (arguments.length < 2)
				token = options.data.token;

			if (! options.quietly && $E.debug) {
				if (H5Utils.isChrome)
					console.info('%c[NgsService - ' + threadId + '] ' + (options.type || 'post').toUpperCase() + ' (' + options.url + ')', 'color:#0d6b2b', options.data)
				else
					console.info('[NgsService - ' + threadId + '] ' + (options.type || 'post').toUpperCase() + ' (' + options.url + ')', options.data)
			}
			options.data = JSON.stringify(options.data);
			var sign = a.signature(options.data, token, digest);
			options.headers = {
				'THREADID' : threadId,
				'TERMINAL' : ($E.admin ? 'admin' : ''),
				'SIGN': sign
			}
			var alwaysCallback = options.always;
			options.always = function() {
				if (busyId)
					a.busyHide(busyId);
				alwaysCallback && alwaysCallback();
			}

			if (options.type == 'get' || options.type == 'GET')
				Ajax.get(options);
			else
				Ajax.post(options);
		},

		signature: function(data, token, digest) {
			var xd = $E.xhrDigest.toLowerCase(), result;
			if (xd == 'sha')
				result = CryptoJS.SHA256(token.substring(digest) + data).toString();
			else if (xd == 'sm3')
				result = sm3(token.substring(digest) + data);
			else
				result = md5(token.substring(digest) + data);
			return result;
		},

		query : function(sqlkey, token, digest, params, callback, col) {
			var a = this, result;
			if ($.type(callback) == 'number') {
				col = callback;
				callback = false;
			}
			a.dataService('SystemDo2', {
				data : $F.sqlbean(sqlkey, token, params),
				async : (callback ? true : false),
				digest : digest,
				success : function(data) {
					if (data.code < 0)
						a.err('Query', data.messaage);
					else {
						if (data.value[0].count > 0) {
							if (col === undefined)
								result = data.value[0].fields[0];
							else if (col < 0)
								retulr = a.makeJsonArray(data.value[0]);
							else
								result = data.value[0].fields[0][col]
						}
						if (callback)
							callback(result);
					}
				}
			})
			if (! callback)
				return result;
		},

		pluginService : function(pluginId, method, options) {
			var pluginService = $E.pluginServices[pluginId];
			if (!pluginService) {
				$F.notify('插件未定义：' + pluginId, -1);
				return;
			}

			var a = this,
				threadId = Date.now(),
				data = JSON.stringify(options.data),
				headers = Object.assign({
					'THREADID' : threadId,
					'TERMINAL' : ($E.admin ? 'admin' : '')
				}, options.headers),
				url,
				start = new Date(),
				busyId = options.silent || $E.silent ? false : a.busyShow(options.title || '');

			if (options.proxy || pluginService.toLowerCase().startWith('http')) {
				// 通过proxy代理
				url = $E.url + '/proxy?plugin=' + pluginId + '&method=' + encodeURIComponent(method);
			} else {
				// 直接访问同一个JVM下的其他WebApp服务
				url = method ? (pluginService + '/' + method) : pluginService;
			}
			if (H5Utils.isChrome)
				console.info('%c[' + pluginId + ' - ' + threadId + '] ' + url, 'color:#0d6b2b');
			else
				console.info('[' + pluginId + ' - ' + threadId + '] ' + url);

			Ajax.post({
				url : url,
				data : data,
				contentType : 'application/json;charset=UTF-8',
				async : options.async,
				cache : false,
				headers : headers,
				success : function(data) {
					var t = (new Date() - start);
					if (t > 500) {
						if (H5Utils.isChrome)
							console.warn('%c[PluginService - ' + threadId + '] ' + method + ', took ' + t + '\'ms', "color:#ff0000;font-size: 1.5em");
						else
							console.warn('[PluginService - ' + threadId + '] ' + method + ', took ' + t + '\'ms');
					}
					//	console.error(data);
					try {
						if ($.isFunction(options.success))
							options.success(data);
					} catch (e) {
						console.error(e)
					}
				},
				fail : function(jqXHR, textStatus, errorThrown) {
					if (jqXHR.status == 200)
						a.err('$F - pluginService', textStatus);
					else
						a.err('$F - pluginService', '[STATUS:' + jqXHR.status + ']', textStatus);
					if ($.isFunction(options.fail)) {
						options.fail(jqXHR, textStatus, errorThrown);
					}
				},
				always : function() {
					if (!options.silent)
						a.busyHide(busyId);
					if ($.isFunction(options.always)) {
						options.always();
					}
				}
			})
		},

		/**
		 * 整理SystemDo2接口返回的数据为JSON对象数组
		 * @method makeJsonArray
		 * @param data {object} 通过SystemDo2获取的数据对象
		 * @param [withMeta] {boolean} 是否将列信息附加在队列上
		 * @param [withTrim] {boolean} 是否对数据字段进行trim，默认为false
		 * @return {array} 标准JSON数据队列
		 */
		makeJsonArray : function(data, withMeta, withTrim) {
			let cs = [];
			if (data.count > 0) {
				$.each(data.fields, function(i, field) {
					let bean = {};
					$.each(data.columns, function(k, column) {
						if (withTrim)
							bean[column] = ($.type(field[k]) == 'string' ? field[k].trim() : field[k]);
						else
							bean[column] = field[k];
					});
					cs.push(bean);
				})
			}
			if (withMeta)
				cs._columns = data.columns;
			return cs;
		},

		/**
		 * 整理SystemDo接口返回的数据为JSON对象对象
		 * @method makeJsonBean
		 * @param data {object} 通过SystemDo2获取的数据对象
		 * @return {array} 标准JSON数据对象
		 */
		makeJsonBean : function(data) {
			var cs = this.makeJsonArray(data);
			return cs.length > 0 ? cs[0] : null;
		},

		/**
		 * 根据传递的主键和数据，直接构造出一个以主键值为key的map对象
		 * @method makeJsonArrayMap
		 * @param data {object} 通过SystemDo2获取的数据对象
		 * @param primaryKey {string} 指定作为主键的字段
		 * @return {object} 符合JSONSQL格式的对象Map
		 */
		makeJsonArrayMap : function(data, primaryKey) {
			var cs = {};
			if (data.count > 0) {
				$.each(data.fields, function(i, field) {
					var bean = {};
					$.each(data.columns, function(k, column) {
						bean[column] = field[k];
					});
					cs[bean[primaryKey.toUpperCase()]] = bean;
				})
			}
			return cs;
		},

		makeJsonPage : function(data, pageIndex) {
			return {
				totalCount: data.count,
				pageCount: data.pages,
				pageIndex: pageIndex,
				elements: this.makeJsonArray(data)
			}
		},

		/**
		 * 调用系统忙碌状态遮布及提示框
		 *
		 * 	// 使用示例
		 * 	let busyId = Guid.raw();	// 创建一个GUID用以标识busy
		 * 	$F.busyShow('正在读取业务数据，请稍后...', busyId, {background:'black', color:'white'});
		 * 	setTimeout(function(){				// 创建10ms的延时，为了显示遮布及提示框
		 *		executeSomeThing(); 			// 处理业务
		 *		$F.busyHide(busyId);			// 业务处理完毕，销毁遮布及提示框
		 * 	}, 10);
		 *
		 * @method busyShow
		 * @param [message] {string} 提示信息
		 * @param [options] {object} 可选参数
		 *  {string} [options.background] 提示框背景色
		 *  {string} [options.color] 提示框字体背景色
		 *  {string} [options.opacity] 遮布透明度
		 *  {string} [options.boxShadow] 提示框阴影
		 */
		busyShow : function(message, options) {
			options = $.extend({
				padding: '8px 23px',
				fontSize: '44px',
				border: 0,
				color: '#61d8ff',
				background: '#444',
				boxShadow: '0px 0px 8px 0px rgba(0,0,0,.4)',
				icon: 'spinner-third'
				//background: "rgb(43, 72, 84)",
				//boxShadow: "0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.2), 0 11px 15px -7px rgba(0,0,0,0.32)"
			}, options)
			let id = Guid.raw();
			Helper.busyShow('', id, options);
			return id;
		},

		busyTitle : function(message) {
			Helper.busyTitle(message);
		},

		/**
		 * 销毁遮布及忙碌提示框
		 * @method busyHide
		 * @param id {string} busy遮罩句柄
		 */
		busyHide : function(id) {
			if (id) {
				Helper.busyHide(id);
			}
		},

		await: function(process) {
			let a = this, waiting = $('#eliteAwait');
			if (waiting.length > 0)
				return;

			waiting = $('<div id="eliteAwait"><span class="inner"><i class="fas fa-spinner-third fa-pulse"></i></span></div>').appendTo($('body'));
			if ($.isFunction(process)) {
				setTimeout(function() {
					process(function() {
						waiting.remove();
					})
				}, 1)
			}
		},

		go: function() {
			var a = this, waiting = $('#eliteAwait');
			if (waiting.length == 0)
				return;
			setTimeout(function() {
				waiting.remove()
			}, 1)
		},

		/**
		 * 异步处理事务方法，系统将在屏幕中进行忙碌状态提示
		 *
		 * 	$F.process(function(){
		 * 		// TODO: 处理业务数据
		 * 		...
		 * 	}, '正在处理...');
		 *
		 * @method process
		 * @param callback {callback} 处理事务的回调方法
		 * @param [message] {string} 处理时提示的消息内容
		 * @param [async] {boolean} 是否采用异步模式
		 * @param [wait] {integer} 延迟执行的毫秒数
		 */
		process : function(callback, message, async, wait) {
			const busyId = $E.progressing || $E.silent ? '' : this.busyShow(message);
			setTimeout(function() {
				if (async) {
					callback(function() {
						$F.busyHide(busyId);
					})
				} else {
					callback();
					$F.busyHide(busyId);
				}
			}, wait||1);
		},

		/**
		 * 异步处理事务方法，系统将在屏幕中进行忙碌状态提示
		 *
		 * 	$processAsync(function(afterProcessCallback){
		 * 		// TODO: 处理业务数据
		 * 		...
		 * 		afterProcessCallback();
		 * 	}, '正在处理...');
		 *
		 * @method processAsync
		 * @param {callback} callback 处理事务的回调方法
		 * @param {string} [message] 处理时提示的消息内容
		 */
		processAsync : function(callback, message) {
			this.process(callback, message, true)
		},

		/**
		 * 在系统底部消息区显示消息
		 *
		 * @method message
		 * @param {string}
		 *            string 消息内容
		 * @param {int}
		 *            state 消息的类型，1=正常，0=警告，-1=错误
		 * @return 显示消息的DOM对象
		 */
		message : function(string, state) {
			if (state == undefined)
				state = 1;
			return $("footer .f_msg").text(string).toggleClass('warn', !state);
		},

		/**
		 * 在系统顶部通知区显示消息
		 * @method notify
		 * @param {string} message 消息内容
		 * @param {number} [state] 消息的类型，1=正常，0=警告，-1=错误
		 * @param {boolean} [wordBreak] 当消息超过提示框宽度时，是否自动换行
		 * @param {number} [boxSize] 消息的框的宽度，像素值
		 * @param {number} [duration] 持续多久(毫秒数)
		 */
		notify : function(message, state, wordBreak, boxSize, duration) {
			var box = $('#notify').text(message);
			if (wordBreak) {
				box.css({
					height: 'auto',
					width: boxSize + 'px',
					whiteSpace: 'normal',
					wordBreak: 'break-word'
				})
			} else {
				box.css({
					height: '2.5em',
					width: '',
					whiteSpace: 'nowrap',
					wordBreak: 'normal'
				})
				box.show();
				box.css({
					// left: ($(window).width() - box.outerWidth()) / 2 + 'px',
					display: 'none'
				})
			}
			box.toggleClass("warn", (state != undefined && state == 0)).toggleClass("error", (state != undefined && state < 0));
			$("body").append(box);
			box.fadeIn(100);
			setTimeout(function() {
				box.fadeOut(500);
			}, duration ? duration : (state != undefined && state <= 0) ? 3000 : 1500);

			return box;
		},

		/**
		 * 打开一个模态的进度条
		 * @method progress
		 * @param {object} [options] 进度条属性参数
		 *  {string} [options.title] 进度条标题
		 *  {integer} [options.width] 进度条宽度
		 * @return {object} 进度条组件
		 *
		 * 	进度条组件对象的属性及方法：
		 * 	var progress = $F.progress({width: 300});
		 * 	progress.value({integer});	// 设置该进度条的进度值，于0至100间
		 * 	progress.finish();	// 完成进度条。进度条窗体自动销毁
		 * 	progress.reset();	// 重值进度条
		 *
		 */
		progress : function(options) {
			return H5Utils.progress(options);
		},

		/**
		 * 针对树控件源数据进行轮循操作
		 * @param data {array} 树控件源数据对象
		 * @param callback {function} 执行轮循的操作回调方法
		 */
		treeEach : function(data, callback) {
			if (!data)
				return;

			var a = this;
			if ($.type(data) == 'array') {
				$.each(data, function(i, v) {
					a.treeEach(v, callback);
				})
			} else {
				if (data.folder)
					a.treeEach(data.children, callback);
				else
					callback(data);
			}
		},

		/**
		 * 对通过SystemDo2接口请求返回的二维数组进行树形结构的整理
		 *
		 * 	// 属性结构数据示例，由于可能存在多个根节点的情况，因此属性结构数据本身是一个ARRAY
		 * 	treedata : [
		 * 		{
		 * 			id: {string},
		 * 			name: {string},
		 * 			pId: {string},
		 * 			folder: {boolean}
		 * 			data: {any},
		 * 			children: [...]
		 * 		}
		 * 		...
		 * 	]
		 *
		 * @method collectTree
		 * @param data {array} SystemDO2接口返回的二维数组 {count:x, columns:[], fields:[]}
		 * @param f_id {number|string} 指定树结构'节点ID'对应的数据表字段名
		 * @param f_pid {number|string} 指定树结构'父节点ID'对应的数据表字段名
		 * @param f_desc {number|string} 指定树结构'节点描述'对应的数据表字段名
		 * @param [forceFolder] {boolean} 是强制是目录
		 * @return {array} 树形结构数据
		 */
		collectTree : function(data, f_id, f_pid, f_desc, forceFolder) {
			var a = this,
				treedata = [],
				tmap = {},
				i_id,
				i_pid,
				i_desc;

			if ($.type(f_id) === 'string') {
				i_id = $F.getIndexOfColumns(data.columns, f_id);
				i_pid = $F.getIndexOfColumns(data.columns, f_pid);
				i_desc = $F.getIndexOfColumns(data.columns, f_desc);
			} else {
				i_id = f_id;
				i_pid = f_pid;
				i_desc = f_desc;
			}
			//console.log(i_id + ", " + i_pid + ", " + i_desc + ", " + i_value);
			$.each(data.fields, function(i, v) {
				var node = {
					id : v[i_id],
					name : v[i_desc],
					pId : v[i_pid],
					children : [],
					folder : !!forceFolder,
					entity : v
				};
				tmap[node.id] = node;
			});
			for (let key in tmap) {
				let node = tmap[key];

				if (!node.pId)
					treedata.push(node);
				else {
					let parentNode = tmap[node.pId];
					if (parentNode) {
						parentNode.folder = true;
						parentNode.children.push(node);
					} else
						a.err('$F - collectTree', '遗失的父节点，ID=' + node.id + ' PARENT:' + node.pId);
				}
			}
			return treedata;
		},

		cleanCheckState: function(data) {
			var a = this;
			$.each(data, function(i, v) {
				delete v._ft_checkState;
				if (v.folder)
					a.cleanCheckState(v.children);
			})
		},

		/**
		 * clear empty node (folder) for tree data
		 */
		cleanTree : function(data) {
			var i = 0;
			while(i < data.length) {
				if (data[i].folder) {
					if  (! data[i].children.length) {
						data.splice(i, 1);
						continue;
					} else
						$F.cleanTree(data[i].children);
				}
				i ++;
			}
		},

		/**
		 * 获取字段序号，在SystemDo2接口中，获得的数据结构中字段名与数据是分离在两个不同的二维数组中，通过字段名获取相应的序号后才能在数据中取得数值。
		 *
		 * 	data : {
		 * 		count: {integer}
		 * 		columns: [....],
		 * 		fields: [[], [], ...]
		 * 	}
		 *
		 * @method getIndexOfColumns
		 * @param {array} columns 字段名数组
		 * @param {string} columnName 字段名
		 * @return {integer} 字段序号，下标为0
		 */
		getIndexOfColumns : function(columns, columnName) {
			for (var i = 0; i < columns.length; i++) {
				if (columns[i].toUpperCase() == columnName.toUpperCase())
					return i;
			}
			return -1;
		},

		/**
		 * 根据GUID获取树形结构中的节点数据
		 * @method getTreeItemById
		 * @param {treedata} treedata 树形结构数据
		 * @param {string} id 查询的节点GUID
		 * @return {treedata} 节点数据对象
		 */
		getTreeItemById : function(treedata, id) {
			var item;
			if ($.type(treedata) == 'array') {
				for (var i = 0; i < treedata.length; i++) {
					item = this.getTreeItemById(treedata[i], id);
					if (item != null)
						return item;
				}
				return null;
			} else {
				if (treedata.id == id)
					return treedata;

				var children = treedata.children;
				if (!children)
					return null;

				for (var i = 0; i < children.length; i++) {
					item = this.getTreeItemById(children[i], id);
					if (item != null)
						return item;
				}
				return null;
			}
		},

		/**
		 * 转义正则特殊字符 （$()*+.[]?\^{},|）
		 *
		 * @param keyword
		 * @return
		 */
		escapeStringRegex : function (keyword) {
			if (keyword)
				return keyword.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
			return keyword;
		},

		parseIcon : function(iconString) {
			if (!iconString)
				return $('<i />');
			var s = iconString.toUpperCase();
			if (s.startWith('HTTP') || s.startWith('/'))
				return $('<img src="' + iconString + '" />');
			if (s.startWith('RES:'))
				return $('<img src="' + iconString.substring(4) + '" />');

			// 自定义图标
			var c,
				icon = iconString.split('#'),
				prefix = '';
			if (icon[0].startWith('!')) {
				prefix += ' vertical';
				icon[0] = icon[0].substring(1);
			}
			if (icon[0].startWith('@')) {
				prefix += ' fa-spin';
				icon[0] = icon[0].substring(1);
			}
			if (icon[0].startWith('~')) {
				prefix += ' pull-right';
				icon[0] = icon[0].substring(1);
			}
			c = this.fas(icon[0]) + prefix;

			if (icon.length > 1)
				return $('<i class="' + c + '" style="color:#' + icon[1] + '"></i>');
			else
				return $('<i class="' + c + '"></i>');
		},

		fas: function(fasName) {
			if (! fasName)
				return 'fa fa-empty';
			else if (fasName.startWith('\\[b\\]'))
				return 'fab fa-' + fasName.substring(3);
			else if (fasName.startWith('\\[l\\]'))
				return 'fal fa-' + fasName.substring(3);
			else if (fasName.startWith('\\[s\\]'))
				return 'fas fa-' + fasName.substring(3);
			else if (fasName.startWith('\\[d\\]'))
				return 'fad fa-' + fasName.substring(3);
			else if (fasName.startWith('\\[\\]'))
				return 'fa fa-' + fasName.substring(3);
			else if (fasName.startWith('{') && fasName.endWith('}'))
				return fasName.substring(1, fasName.length - 1);
			else
				return 'far fa-' + fasName;
		},

		/**
		 * 实现项目资源中配置的多语言替换
		 */
		i18n: function(content, resource) {
			if (! content)
				return '';
			if (! resource)
				return content;

			var matches = content.match(/\{\{([\u4e00-\u9fa5 | \w | \. | @ | %]+)\}\}/gi);
			if (matches) {
				for(var i = 0; i<matches.length; i++ ) {
					content = content.replace(matches[i], function(){
						var ex = (/\{\{([\u4e00-\u9fa5 | \w | \. | @ | %]+)\}\}/gi).exec(matches[i]);
						if (resource.hasOwnProperty(ex[1]))
							return resource[ex[1]];
						else
							return ex[1];
					})
				}
			}
			return content;
		},

		/**
		 * JavaScript静态资源文件的多语言替换
		 */
		lang: function(key) {
			if (! window.i18n)
				return key;
			return this.getProperty(window.i18n, key) || key;
		},

		firstMapEntry : function(map) {
			for (var key in map) {
				return {
					key : key,
					value : map[key]
				}
			}
		},

		lazyImg : function(img, src, callback) {
			var img = $(img),
				loading = $('<div><img class="loadingImg" src="' + $E.url + '/images/loading.gif"/></div>');
			loading.css({
				position : img.css('position'),
				top : img.css('top'),
				left : img.css('left'),
				width : img.outerWidth(),
				height : img.outerHeight(),
				maxWidth : img.css('maxWidth'),
				maxHeight : img.css('maxHeight')
			})
			img.after(loading);
			img.hide();

			var loadImg = new Image();
			loadImg.src = src;
			$(loadImg).on('load', function(e) {
				loading.remove();
				img.attr('src', loadImg.src);
				img.show();

				if ($.isFunction(callback))
					callback(img);
			})
		},

		escapeSpecialCharForSelector : function(source) {
			if (source && $.type(source) === 'string') {
				return source.replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, "\\$&");
			}
			return source;
		},

		/**
		 * 获取配置的规则表达式，动态页面中，可以直接用：
		 * var ruleExpression = window.$F.fetchRuleExpression($project, 'xxxxxx');
		 * if (ruleExpression) {
		 *      R.xxx = evalCmd(ruleExpression);
		 * }
		 * 这样直接获取到规则表达式在当前动态页面环境中执行的结果
		 * @param project
		 * @param ruleId
		 * @returns {string}
		 */
		fetchRuleExpression : (project, ruleId) => {
			var rules = project.getCache('RULE_CODE');
			if (!rules) {
				rules = {};
				project.putCache('RULE_CODE', rules);
			}
			var rule = rules[ruleId];
			if (!rule) {
				var ret = project.commonQuerySync('ECore.RuleCode.query', [ ruleId ]);
				if (ret && ret.value[0].count > 0) {
					rule = $F.makeJsonBean(ret.value[0]);
				}
				if (!rule) {
					return;
				}
				rules[ruleId] = rule;
			}
			var result = {
				value : rule.RULE_TYPE === 1 ? [] : ''
			};
			try {
				var ifElseJson = JSON.parse(rule.IFELSE);
				const parseConditions = (rule, result) => {
					try {
						if (rule.conditions && rule.conditions.length > 0) {
							rule.conditions.forEach((cond, i) => {
								if (i === 0) {
									if (cond.conditions) {
										// result.value += '(';
										parseConditions(cond, result);
										// result.value += ')';
									} else {
										if (rule.symbols[i] === 'isEmpty' || rule.symbols[i] === 'isNotEmpty') {
											result.value += '$RF.' + rule.symbols[i] + '(' + fetchData(cond) + ')';
										} else if (rule.symbols[i] === 'assign') {
											result.value += '$RF.' + rule.symbols[i] + '("' + fetchData(cond) + '", ' + fetchData(rule.conditions[i + 1]) + ')';
										} else {
											if (rule.conditions[i + 1] && rule.conditions[i + 1].type === 4) {
												result.value += '$RF.dateDiff(' + fetchData(cond) + ', "' + rule.symbols[i] + '", '+ fetchData(rule.conditions[i + 1]) + ')';
											} else {
												result.value += fetchData(cond);
											}
										}
									}
								} else {
									if (rule.symbols[i - 1] === 'isEmpty' || rule.symbols[i - 1] === 'isNotEmpty' || rule.symbols[i - 1] === 'assign') {
										// 是否有值直接用函数比较
									} else {
										if (cond.conditions) {
											result.value += rule.symbols[i - 1];
											result.value += '(';
											parseConditions(cond, result);
											result.value += ')';
										} else {
											if (cond.type === 4) {
												// 日期类型用dateDiff比较
											} else {
												result.value += rule.symbols[i - 1] + fetchData(cond);
											}
										}
									}
								}
							});
						}
					} catch (e) {
						console.error(e);
					}
				};
				const fetchData = condition => {
					if (typeof (condition) === 'object') {
						if (condition.id) { // 如果是key
							return condition.value;
						} else {
							switch (condition.type) {// 0 字符输入 1 数字输入 2 日期 3 下拉选择
								case 0:
								case 1:
									return condition.value;
								case 2:
								case 3: // 下拉值
									return '"' + condition.value + '"';
								case 4:// 时间
									// {"value": "45", "unit": "y", "type": 4}
									return '"' + condition.value + condition.unit + '"';
							}
						}
					} else {
						if (typeof (condition) === 'string') {
							// if (isNaN(condition)) {
							// return '"' + condition + '"';
							// } else {
							return condition;
							// }
						} else {
							return condition;
						}
					}
				};
				if (ifElseJson.code && ifElseJson.code.length) {
					if (rule.RULE_TYPE === 1) {
						ifElseJson.code.forEach((c, i) => {
							var oneCon = {
								value: ''
							};
							oneCon.value += 'if(';
							if (c.alwaysTrue) {
								oneCon.value += 'true';
							} else {
								if (c.codeIf) {
									oneCon.value += c.codeIf;
								} else {
									parseConditions(c.rule, oneCon);
								}
							}
							oneCon.value += '){';
							if (c.thenRule) {
								parseConditions(c.thenRule, oneCon);
							} else {
								oneCon.value += c.codeReturn;
							}
							oneCon.value += '}';
							result.value.push(oneCon.value);
						});
					} else {
						ifElseJson.code.forEach((c, i) => {
							//每个code代表一行if
							if (i === 0) {
								result.value += 'if(';
							} else {
								result.value += 'else if(';
							}
							parseConditions(c.rule, result);
							result.value += '){';
							result.value += c.codeReturn;
							result.value += '}';
						});
					}
				}
			} catch (e) {}
			return result.value;
		},

		/**
		 * 用post提交一个表单到新打开的一个窗口
		 * @param url
		 * @param data 格式为一个数组，没个数组元素需要有name和value属性
		 * [
		 * 	{
		 * 		name: 'a1', value: 'bbb'
		 * 	},
		 *  {
		 * 		name: 'a2', value: 'ccc'
		 * 	}
		 * ]
		 * @param name 表示打开窗口名字，需要自己指定一个名字
		 */
		openWindowPost : function(url, data, target) {
			if (url) {
				target = target || 'wp';
				var tempForm = $('<form id="tempForm" method="POST" action="' + url + '" target="' + target + '">');
				$.each(data, function(i, v) {
					// var hideInput = $('<input type="hidden" name="' + v.name + '" value="' + v.value + '" >');
					var hideInput = $('<textarea style="display:none;" rows="90" cols="50" name="' + v.name + '" >' + v.value + '</textarea> ');
					tempForm.append(hideInput);
				});
				window.open('about:blank', target);

				$("body").append(tempForm);
				//tempForm.trigger("submit");
				tempForm.submit();
				$("body").remove(tempForm);
			}
		},

		/**
		 * 计算地球表面两点(经纬度)间距离
		 * baiduMapGetDistance(lat1,lng1,lat2,lng2)
		 * @param lat1 {float}	经度1
		 * @param lng1 {float}  纬度1
		 * @param lat2 {float}	经度2
		 * @param lng2 {float}	纬度2
		 */
		baiduMapGetDistance : function(lat1, lng1, lat2, lng2) {
			var EARTH_RADIUS = 6378137.0; //单位M
			var f = (lat1 + lat2) / 2 * Math.PI / 180;
			var g = (lat1 - lat2) / 2 * Math.PI / 180;
			var l = (lng1 - lng2) / 2 * Math.PI / 180;
			var sg = Math.sin(g);
			var sl = Math.sin(l);
			var sf = Math.sin(f);
			var s,
				c,
				w,
				r,
				d,
				h1,
				h2;
			var a = EARTH_RADIUS;
			var fl = 1 / 298.257;
			sg = sg * sg;
			sl = sl * sl;
			sf = sf * sf;
			s = sg * (1 - sl) + (1 - sf) * sl;
			c = (1 - sg) * (1 - sl) + sf * sl;
			w = Math.atan(Math.sqrt(s / c));
			r = Math.sqrt(s * c) / w;
			d = 2 * w * a;
			h1 = (3 * r - 1) / 2 / c;
			h2 = (3 * r + 1) / 2 / s;
			var distance = d * (1 + fl * (h1 * sf * (1 - sg) - h2 * (1 - sf) * sg));
			return distance;
		},

		/**
		 * 根据经度维度返回具体信息，包括省、市、区、街道信息
		 * @param longitude {String}	经度
		 * @param latitude {String}  纬度
		 * @param lat2 {function}	回调方法
		 */
		bMapGgetPointDesc : function(longitude, latitude, callback) {
			var point = new BMap.Point(longitude, latitude);
			var myGeo = new BMap.Geocoder();
			myGeo.getLocation(point, function(result) {
				if (callback) {
					return callback(result);
				}
			});
		},

		/**
		 * 获取当前地点的地理位置
		 * param:callback 回调， 获取到的地理位置的详细信息返回
		 */
		getCurrentPosition : function(callback) {
			navigator.geolocation.getCurrentPosition(function(position) {
				$F.bMapGgetPointDesc(position.coords.longitude, position.coords.latitude, callback);
			});
		},

		/**
		 * 将beans队列改造为包含label，value字段的beans对象队列，以适用于wisCombox
		 */
		comboxBeans : function(beans, options) {
			var cs = [];
			$.each(beans, function(i, v) {
				if (options.refit) {
					v.label = (options.label ? options.label(v) : v.label);
					v.value = (options.value ? options.value(v) : v.value);
				} else {
					cs.push({
						label: (options.label ? options.label(v) : v.label),
						value: (options.value ? options.value(v) : v.value),
						entity: v
					})
				}
			})
			return options.refit ? beans : cs;
		},

		parseInt : function(v) {
			try {
				v = parseInt(v);
				if (!isNaN(v)) {
					return v;
				} else {
					return 0;
				}
			} catch (e) {
				return 0;
			}
		},

		parseFloat : function(v) {
			try {
				v = parseFloat(v);
				if (!isNaN(v)) {
					return v;
				} else {
					return 0;
				}
			} catch (e) {
				return 0;
			}
		},

		getProperty : function(object, path) {
			if (object === undefined)
				return undefined;

			var cs = path.indexOf('.');
			if (cs == -1)
				return object[path];

			return this.getProperty(object[path.substring(0, cs)], path.substring(cs+1));
		},

		/**
		 * 根据指定的掩码格式来做字符串的屏蔽
		 * @param string {string} 需要屏蔽的字符串
		 * @param mask {string} 掩码格式，例如 '3,5' 代表从第三个字符开始连续屏蔽5位
		 * @return {string} 屏蔽后的字符串
		 */
		shieldMask : function(string, mask) {
			if (!string)
				return '';

			string = string.toString();
			if (mask) {
				var cs = mask.split(','),
					start = parseInt(cs[0]),
					end;
				var visible = string.substring(0, start - 1);
				if (string.length >= start && start > 0) {
					if (cs.length == 1) {
						var mask = '';
						for (var i = 0; i < string.length - start + 1; i++) {
							if (mask.length < 6)
								mask += '*'
						}
						return (visible + mask);
					} else {
						end = parseInt(cs[1]);
						var mask = '',
							end = Math.min(end, string.length - start + 1);
						for (var i = 0; i < end; i++) {
							if (mask.length < 6)
								mask += '*'
						}
						var after = (string.length >= (start - 1 + end)) ? string.substring(start - 1 + end) : '';
						return (visible + mask + after);
					}
				}
			}
			return string;
		},

		/**
		 * 根据系统参数来屏蔽相关内容
		 * @param targetNumber 需要屏蔽的号码，也可以是其他任何字符串
		 * @param TELWLD
		 * @param TELSHD
		 * @param TELWLF
		 * @param TELSHR
		 */
		shieldNumber : function(targetNumber, TELWLD, TELSHD, TELWLF, TELSHR) {
			if (targetNumber) {
				targetNumber = targetNumber.toString().trim();
				var xxx = "";
				if (TELWLF) {
					try {
						var ruleObj = {};
						var ruleArray = TELWLF.split("|");
						for (i = 0; i < ruleArray.length; i++) {
							var rule = ruleArray[i];
							if (rule.indexOf("=") > -1) {
								var ruleKey = rule.substring(0, rule.indexOf("="));
								var ruleValue = rule.substring(rule.indexOf("=") + 1);
								ruleObj[ruleKey.toUpperCase()] = ruleValue;
							}
						}
						var ruleStr = null;
						if (targetNumber.length == 6 && ruleObj.T6) {
							ruleStr = ruleObj.T6;
						}
						if (targetNumber.length == 7 && ruleObj.T7) {
							ruleStr = ruleObj.T7;
						}
						if (targetNumber.length == 8 && ruleObj.T8) {
							ruleStr = ruleObj.T8;
						}
						if (targetNumber.length >= 9 && ruleObj.T9) {
							ruleStr = ruleObj.T9;
						}
						if (Verify.isMobile(targetNumber) && ruleObj.M) {
							ruleStr = ruleObj.M;
						}
						if (ruleStr == null && ruleObj.T) {
							ruleStr = ruleObj.T;
						}
						if (ruleStr && ruleStr.indexOf(",") > 0) {
							var numRangeArr = ruleStr.split(",");
							for (i = 0; i < numRangeArr[0]; i++) {
								xxx += "*";
							}
							targetNumber = targetNumber.substring(0, targetNumber.length - numRangeArr[0] - numRangeArr[1])
								+ xxx + targetNumber.substring(targetNumber.length - numRangeArr[1]);
						}
					} catch (e) {
						$F.err("$F - shieldNumber", "error occurred on shieldNumber with TELWLF: " + e.message);
					}
				} else {
					if (TELSHR && TELSHR > 0 && targetNumber.length < TELSHR) {
						return targetNumber;
					}
					var i = 0;
					var targetNumLength = targetNumber.length;
					//handle TELSHD
					if (TELSHD) {
						if (TELSHD.indexOf(",") > 0) {
							var start = parseInt(TELSHD.substring(0, TELSHD.indexOf(","))) - 1;
							var end = parseInt(TELSHD.substring(TELSHD.indexOf(",") + 1, TELSHD.length)) - 1;
							if (start > end) {
								end = start + end;
								start = end - start;
								end = end - start;
							}
							if (start >= targetNumLength) {

							} else {
								if (end <= targetNumLength) {
									for (i = 0; i <= end - start; i++) {
										xxx += "*";
									}
									targetNumber = targetNumber.substring(0, start) + xxx + targetNumber.substring(end + 1, targetNumLength);
								} else {
									for (i = 0; i <= targetNumLength - start; i++) {
										xxx += "*";
									}
									targetNumber = targetNumber.substring(0, start) + xxx;
								}
							}
							return targetNumber;
						}
					}

					//handle TELWLD
					if (TELWLD) {
						if (targetNumber.length <= TELWLD) {
							targetNumber = "";
						} else {
							targetNumber = targetNumber.substring(0, targetNumber.length - TELWLD);
						}
						for (i = 0; i < TELWLD; i++) {
							targetNumber += "*";
						}
					}
				}
			}
			return targetNumber;
		},

		/**
		 * @param password
		 * @param parent
		 * @param forNew
		 */
		encPwd: function(password, parent, forNew) {
			if (! password)
				return '';

			var encode = password;
			if (!forNew) {
				//如果是修改密码时候，新密码不能存成md4，默认直接明文方式存储
				switch(parent.enc) {
					case 'md5':
						encode = '[MD5]' + md5(password);
						break;
					case 'md4':
						encode = '[MD4]' + md5(md5(password) + parent.uid);
						break;
					case 'sha':
						encode = '[SHA]' + CryptoJS.SHA256(password).toString();
						//encode = '[SHA]' + CryptoJS.SHA256(CryptoJS.SHA256(password).toString() + parent.uid).toString();
						break;
					case 'sm3':
						encode = '[SM3]' + sm3(password);
						break;
					default:
						encode = password;
						break;
				}
			}
			if (parent.pwdEncWithRSA && parent.rsaPubKey) {
				// Encrypt with the public key...
				var encrypt = new JSEncrypt();
				encrypt.setPublicKey(parent.rsaPubKey);
				return '[RSA]' + encrypt.encrypt(encode);
			}
			return encode;
		},

		Masker: {
			desensitization (source) {
				if ($.type(source) === 'string')
					return this.execute(source);
				const results = [];
				source.forEach(data => {
					results.push(this.execute(data))
				})
				return results;
			},

			noPassByName(name) {
				if (! name)
					return '';

				if (name.length <= 3) {
					return "*" + name.substring(1, name.length);
				} else if (name.length > 3 && name.length <= 6){
					return "**" + name.substring(2, name.length);
				} else {
					return name.substring(0,2) + "****" + name.substring(6, name.length)
				}
			},

			execute(source) {
				// 正则判断返回相应数据
				if (/(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(source)
					|| /^(13[0-9]|16[0-9]|19[0-9]|147|15[0-9]|17[6-8]|18[0-9])\d{8}|17[0-9]\d{8}$/.test(source)
					|| /(^(?:(?![IOZSV])[\dA-Z]){2}\d{6}(?:(?![IOZSV])[\dA-Z]){10}$)|(^\d{15}$)/.test(source)) {
					// 身份证号 || 手机号  ||  营业执照    前三后四
					return source.substr(0,3) + "****" + source.substr(-4);
				} else if(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/.test(source)) {
					// 邮箱号码  前二 后以 @ 分割
					return source.substr(0,2) + "****" + source.substr(source.indexOf('@'));
				} else if(/^\d{16}|\d{19}$/.test(source)) {
					// 银行卡号  后四位
					return "******" + source.substr(-4);
				} else {
					return source;
				}
			},

			formatMask(source, start, end) {
				if (source == null || source == undefined)
					return '';

				if ($.type(source) !== 'string')
					source = source + '';
				if (start + end >= source.length)
					return source;

				// let length = source.length - start - end;
				// let replace = "$1" + this.appendStr(length) + "$3";
				let replace = "$1***$3";
				return source.replaceAll("(\\w{"+start+"})(\\w+)(\\w{"+end+"})", replace);
			},

			appendStr(length) {
				let value = '';
				for (let i = 0; i < length; i++) {
					value += '*';
				}
				return value;
			}
		},

		Security: {
			/**
			 * AES加密
			 * @method aesEncrypt
			 * @param content {string} 需要加密的字符串
			 * @param [password] {string} 加密秘钥
			 * @param [local] {boolean} 是否是本地加密
			 * @return {string}
			 */
			aesEncrypt : function(content, password) {
				if (content === undefined)
					return $F.err('Security - aesEncrypt', '加密字符串未定义'), '';

				return CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(content), CryptoJS.enc.Utf8.parse(password), {
					mode: CryptoJS.mode.ECB,
					padding: CryptoJS.pad.Pkcs7,
					iv: CryptoJS.enc.Utf8.parse('')
				}).ciphertext.toString()
			},

			/**
			 * AES解密
			 * @method aesDecrypt
			 * @param content {string} 需要解密的字符串
			 * @param [password] {string} 解密秘钥
			 * @param [local] {boolean} 是否是本地解密
			 * @return {string}
			 */
			aesDecrypt : function(content, password) {
				if (content === undefined)
					return $F.err('Security - aesDecrypt', '加密字符串未定义'), '';

				let encryptedHexStr = CryptoJS.enc.Hex.parse(content);
				let encryptedBase64Str = CryptoJS.enc.Base64.stringify(encryptedHexStr);
				// 解密
				let decryptedData = CryptoJS.AES.decrypt(encryptedBase64Str, CryptoJS.enc.Utf8.parse(password), {
					mode: CryptoJS.mode.ECB,
					padding: CryptoJS.pad.Pkcs7
				});
				return decryptedData.toString(CryptoJS.enc.Utf8);
			},

			/**
			 * DES加密
			 * @method desEncrypt
			 * @param content {string} 需要加密的字符串
			 * @param [password] {string} 加密秘钥
			 * @return {string}
			 */
			desEncrypt : function(content, password) {
				if (content == undefined)
					return $F.err('Security - desEncrypt', '加密字符串未定义'), '';

				return CryptoJS.DES.encrypt(CryptoJS.enc.Utf8.parse(content), CryptoJS.enc.Utf8.parse(password), {
					mode: CryptoJS.mode.ECB,
					padding: CryptoJS.pad.Pkcs7
				}).ciphertext.toString();
			},

			/**
			 * DES解密
			 * @method desDecrypt
			 * @param content {string} 需要解密的字符串
			 * @param [password] {string} 解密秘钥
			 * @return {string}
			 */
			desDecrypt : function(content, password) {
				if (content == undefined)
					return $F.err('Security - desDecrypt', '加密字符串未定义'), '';

				return CryptoJS.DES.decrypt(CryptoJS.enc.Hex.parse(content).toString(), CryptoJS.enc.Utf8.parse(key), {
					mode: CryptoJS.mode.ECB,
					padding: CryptoJS.pad.Pkcs7
				}).toString(CryptoJS.enc.Utf8)
			},

			/**
			 * SHA-256加密
			 * @method shaEncrypt
			 * @param content {string} 需要加密的字符串
			 * @param [password] {string} 加密秘钥
			 * @return {string}
			 */
			shaEncrypt : function(content, password) {
				if (content == undefined)
					return $F.err('Security - shaEncrypt', '加密字符串未定义'), '';

				var result = null;
				Ajax.post({
					url : $E.url + '/commons/sha256Encrypt_',
					data : {
						content : content,
						password : password || ''
					},
					success : function(data) {
						if (data.code > 0)
							result = data.value;
						else
							$F.alert(data.message);
					},
					async : false
				})
				return result;
			},

			/**
			 * SHA1加密
			 * @method shaEncrypt
			 * @param content {string} 需要加密的字符串
			 * @return {string}
			 */
			sha1Encrypt : function(content) {
				if (content == undefined)
					return $F.err('Security - sha1Encrypt', '加密字符串未定义'), '';

				var result = null;
				Ajax.post({
					url : $E.url + '/commons/sha1Encrypt_',
					data : {
						content : content
					},
					success : function(data) {
						if (data.code > 0)
							result = data.value;
						else
							$F.alert(data.message);
					},
					async : false
				})
				return result;
			},

			/**
			 * 凯撒加密算法，用于低安全的无秘钥加密
			 * @method kasDecrypt
			 * @param content {string} 需要加密的字符串
			 * @return {string}
			 */
			kasEncrypt : function(content) {
				if (content == undefined)
					return $F.err('Security - kasEncrypt', '加密字符串未定义'), '';

				var result = null;
				Ajax.post({
					url : $E.url + '/commons/kasEncrypt_',
					data : {
						content : content
					},
					success : function(data) {
						if (data.code > 0)
							result = data.value;
						else
							$F.alert(data.message);
					},
					async : false
				})
				return result;
			},

			/**
			 * 凯撒解密算法
			 * @method kasEncrypt
			 * @param content {string} 需要解密的字符串
			 * @return {string}
			 */
			kasDecrypt : function(content) {
				if (content == undefined)
					return $F.err('Security - kasDecrypt', '解密字符串未定义'), '';

				var result = null;
				Ajax.post({
					url : $E.url + '/commons/kasDecrypt_',
					data : {
						content : content
					},
					success : function(data) {
						if (data.code > 0)
							result = data.value;
						else
							$F.alert(data.message);
					},
					async : false
				})
				return result;
			},

			/**
			 * RSA以公钥进行的加密算法
			 * @method rsaDecrypt
			 * @param content {string} 被加密字符串
			 * @return {string}
			 */
			rsaEncrypt : function(content) {
				// Encrypt with the public key...
				const encrypt = new JSEncrypt();
				encrypt.setPublicKey($E.rsaPubKey);
				return encrypt.encrypt(content);
			},

			/**
			 * RSA以公钥进行的解密算法
			 * @method rsaEncrypt
			 * @param content {string} 被解密的字符串
			 * @return {string}
			 */
			rsaDecrypt : function(content) {
				// Encrypt with the public key...
				const encrypt = new JSEncrypt();
				encrypt.setPublicKey($E.rsaPubKey);
				return encrypt.decrypt(content);
			},

			/**
			 * RSA以公钥进行的解密算法
			 * @method rsaEncrypt
			 * @param content {string} 被解密的字符串
			 * @return {string}
			 */
			rsaDecryptByServer : function(content) {
				let result = '';
				Ajax.post({
					url : $E.url + '/commons/rsaDecrypt',
					data : {
						content : content,
					},
					success : function(data) {
						if (data.code > 0)
							result = data.value;
						else
							$F.alert(data.message);
					},
					async : false
				})
				return result;
			},

			/**
			 * 对数据进行脱敏加解密，加解密方式及秘钥由系统配置决定
			 * @param content {array|string} 需要进行加解密的内容
			 * @param [decrypt] {boolean} 是否进行解密操作，默认为false
			 * @param [project] {object} 当前项目
			 * @return {string} 完成加解密后的字符串
			 */
			dbMasker : function(content, decrypt, project) {
				project = project || $E.getActiveProject();
				if (! content)
					return '';
				let ENCRIP = project.getParam("ENCRIP"); // 从系统参数获取是否用本地aes加密
				if (ENCRIP) {
					try {
						var encInfo = JSON.parse(ENCRIP);
						if (encInfo.local === true) {
							if (encInfo.type === 'AES') {
								if ($.isArray(content)) {
									let rets = [];
									content.forEach(c => {
										if (decrypt) {
											rets.push(this.aesDecrypt(c, encInfo.key, true))
										} else {
											rets.push(this.aesEncrypt(c, encInfo.key, true))
										}
									});
									return rets;
								} else {
									if (decrypt) {
										return this.aesDecrypt(content, encInfo.key, true)
									} else {
										return this.aesEncrypt(content, encInfo.key, true)
									}
								}
							}
						}
					} catch (e) {
						$F.log('$F - dbMasker', 'Operate ENCRIP failed')
					}
				}
				$F.ngsService({
					async: false,
					url: $E.url + '/commons/dbmasker_',
					data: {
						content : content,
						action: (decrypt ? 'decrypt' : 'encrypt'),
					},
					success: function(data) {
						if (data.code > 0)
							content = data.value;
						else
							$F.alert(data.message);
					}
				}, project.token, project.digest);
				return content;
			}
		},

		/**
		 * 翻译clientFlag
		 * @param clientFlag
		 * @return {*}
		 */
		parseClientFlag: function(clientFlag) {
			switch(clientFlag) {
				case 0:
					return $F.lang('clientFlag.utility');
				case 1:
					return $F.lang('clientFlag.agent');
				case 2:
					return $F.lang('clientFlag.mobile');
				case 3:
					return $F.lang('clientFlag.wo');
				case 4:
					return $F.lang('clientFlag.admin');
				case 5:
					return $F.lang('clientFlag.softphone');
				case 7:
					return $F.lang('clientFlag.crm');
				default:
					return clientFlag || 'unknown';
			}
		},

		/**
		 * 根据ip字符串地址获取对应的long数值（只支持ipv4）
		 * @param ip ip字符串
		 */
		ip2Long: function(ip) {
			var multipliers = [0x1000000, 0x10000, 0x100, 1];
			var longValue = 0;
			ip.split('.').forEach(function(part, i) {
				longValue += part * multipliers[i];
			});
			return longValue;
		},

		/**
		 * 防抖功能, 在等待的时长内，再次调用方法将不会重复触发该方法（注意：传入的func方法对象需要是同一个对象）
		 * @param func 不要包装的方法
		 * @param delay 等待的时长
		 * @param immediate 是否立即执行
		 * @returns {function(...[*]=)}
		 */
		debounce: function(func, delay = 500, immediate = true) {
			// return function (args) {
			// 	let that = this, _args = args;
			// 	clearTimeout(fun.id);
			// 	fun.id = setTimeout(function () {
			// 		fun.call(that, _args)
			// 	}, delay);
			// }

			return function () {
				let context = this, args = arguments;
				if (func.id) clearTimeout(func.id);
				if (immediate) {
					// 如果已经执行过，不再执行
					var callNow = !func.id;
					func.id = setTimeout(function() {
						func.id = null;
					}, delay);
					if (callNow) func.apply(context, args)
				} else {
					func.id = setTimeout(function() {
						func.apply(context, args)
					}, delay);
				}
			}
		},

		nextTick: function (callback) {
			if (callback) {
				setTimeout(() => {
					callback();
				}, 0);
			}
		},

		/**
		 * 图片预览功能
		 * @param photo 图片地址
		 */
		imageView (photo) {
			if(!$F.isEmpty(photo)){
			if (photo.toLowerCase().startWith('res:'))
				photo = $E.url + '/' + photo;
			else if (photo.toLowerCase().startWith('http:')) {
				photo = photo
			} else {
				if (!photo.toLowerCase().startWith('http'))
					photo = $E.url + '/fs/get?file=' + photo;
			}
			}

			var img = $('#_imgviewer_elite');
			if (img.length === 0)
				img = $('<img id="_imgviewer_elite" style="width:0;height:0;position:absolute;display:none" />').appendTo('body');
			img.attr('src', photo);
			img.viewer().trigger('click');
		},
		transferToMergedTableDom: function (list,lastcolfilter) {
			list = list;
			ObjList = [];
			lastcolfilter = lastcolfilter;
			for (var i = 0; i < list.length; i++) {
				ListB = [];
				for (var j = 0; j < list[0].length; j++) {
					var ListA = {
						content: list[i][j],
						row: 1,
						col: 1
					}
					ListB.push(ListA);
				}
				ObjList.push(ListB);
			}

			i = 0;
			j = 0;

			for (var i in ObjList) {
				for (var j in ObjList[i]) {
					for (var num = 1; j < ObjList[0].length - num && ObjList[i][j].content === ObjList[i][+ j + num * 1].content; num++) {
						//if (j >= ObjList[0].length - lastcolfilter) continue;
						if (j < ObjList[0].length - lastcolfilter) {
							ObjList[i][j].col++;
						}
					}
					if (j > 0 && j < ObjList[0].length - lastcolfilter && ObjList[i][j].content === ObjList[i][+ j - 1].content) {
						ObjList[i][j].col = 0;
					}
					// if (j == lastcolfilter){
					// 	ObjList[i][j].col = 1;
					// 	ObjList[i][j].row = 1;
					// }
				}
				for (var m in ObjList[i]) {
					for (var num = 1; i < ObjList.length - num && ObjList[i][m].content === ObjList[+i + num * 1][m].content; num++) {
						//if (j < ObjList[0].length - lastcolfilter) continue;
						ObjList[i][m].row++;
					}
					if (i > 0 && ObjList[i][m].content === ObjList[+ i - 1][m].content) {
						ObjList[i][m].row = 0;
					}
					// if (colfilter.includes(i)){
					// 	ObjList[i][j].row = 1;
					// }
				}
			}
			for (var n = 0;n < ObjList.length;n++)
				if (lastcolfilter) {
					for (var k = + ObjList[0].length - lastcolfilter;k < ObjList[0].length;k++) {
						ObjList[n][k].col = 1;
						ObjList[n][k].row = 1;
					}
				}
			return ObjList;
		}
	};

	window.AsyncProc = {
		each : function(array, method, finalFunction, index) {
			const a = this;
			if (index === undefined)
				index = 0;
			if ($.type(array) !== 'array' || array.length == 0 || index >= array.length)
				finalFunction();
			else {
				const data = array[index]
				index++;
				try {
					method(data, function(e, error) {
						if (error) {
							finalFunction(e, error);
						} else {
							if (index < array.length)
								a.each(array, method, finalFunction, index);
							else
								finalFunction();
						}
					}, index);
				} catch (e) {
					console.error(e);
				}
			}
		},

		eachOf : function(object, method, finalFunction) {
			this.each(Object.keys(object), method, finalFunction);
		},

		waterfall : function(jobs, finalFunction) {
			var a = this,
				index = 0;
			if ($.type(jobs) !== 'array' || jobs.length == 0)
				finalFunction();
			else {
				var next = function() {
					jobs[index](function() {
						index++;
						if (index < jobs.length)
							next();
						else {
							finalFunction();
						}
					})
				};
				next();
			}
		},
	};

	window.alert = function() {
		$F.alert.apply($F, arguments)
	};
	window.$F = $F;
})(jQuery);
